From 7602ef63b56232bcc61ff55c3e40cf55fde343d9 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Wed, 17 Jun 2026 12:24:14 +0530 Subject: [PATCH 1/2] Add edgezero extensible-cli repin finding and implementation plan --- .../plans/2026-06-17-edgezero-269-repin.md | 427 ++++++++++++++ ...edgezero-269-repin-breaking-api-finding.md | 557 ++++++++++++++++++ 2 files changed, 984 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-17-edgezero-269-repin.md create mode 100644 docs/superpowers/specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md diff --git a/docs/superpowers/plans/2026-06-17-edgezero-269-repin.md b/docs/superpowers/plans/2026-06-17-edgezero-269-repin.md new file mode 100644 index 00000000..43118c19 --- /dev/null +++ b/docs/superpowers/plans/2026-06-17-edgezero-269-repin.md @@ -0,0 +1,427 @@ +# EdgeZero #269 Repin Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Repin trusted-server's edgezero dependency from its current pin (`38198f9` on the PR14 base; `170b74b` is the stack's _original_ PR1–13 pin) to post-#269 and fix the only forced code break (`Body::into_bytes` → `Option`), keeping the bespoke `platform/` layer unchanged. + +**Architecture:** Mechanical dependency bump on a dedicated branch off `feature/edgezero-pr14-entry-point-dual-path` (never in-place on a reviewed PR), then propagate up the stack by merge. The "test" for this work is the compiler + full CI gate: RED = build errors at `Body` sinks, GREEN = full gate passes. No new abstractions; the A/B/C convergence (store registry, typed `AppConfig`, entry-point) is **out of scope** — separate follow-on plans. + +**Tech Stack:** Rust 2024, cargo, `wasm32-wasip1` (Fastly via Viceroy), edgezero git dep, `error-stack`. + +**Source spec:** [2026-06-16-edgezero-269-repin-breaking-api-finding.md](../specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md) — §2 (sink list), §5 (steps), §8 (gate), §11 (stack/merge-up). + +--- + +## Scope & non-goals + +**In scope:** repin the 4 `edgezero-*` deps; fix the 18 `Body::into_bytes` sinks (8 production + 10 test); reconcile the integration-tests lockfile; drop the two never-compiled adapter deps; pass the full gate; merge up PR14→PR20. + +**Out of scope (separate plans):** store convergence onto edgezero `ConfigStore`/`SecretStore`/`StoreRegistry` (spec §6 B); typed `AppConfig` two-tier config (§6 C / Christian's CLI port); entry-point `run_app`/`app!` adoption (§6 C). Do **not** start these here. + +**Key constraints:** + +- `as_bytes` changed but has **no** trusted-server sink — only `into_bytes` needs edits (spec §2). +- All sinks are buffered (`Once`) bodies → fix with `.expect("should …")`, never `unwrap_or_default()`. +- Line numbers below are **PR14-base**; they shift per branch. **Re-derive the exact sink set from the compiler on each branch** — do not trust hardcoded lines after PR14. +- Pin to the #269 HEAD sha while #269 is open; **re-pin to edgezero `main` after #269 merges**. + +--- + +## File structure + +| File | Change | Responsibility | +| ------------------------------------------------------------- | ---------------------------------------------------- | --------------------------- | +| `Cargo.toml` | Modify lines 59–62 | The 4 `edgezero-*` git pins | +| `Cargo.lock` | Regenerated | Root lock | +| `crates/integration-tests/Cargo.lock` | Reconcile | Shared-dep lock (CI gate) | +| `crates/trusted-server-core/src/proxy.rs` | 5 sinks (38, 1550, 1665 prod; 2034, 2795, 2851 test) | proxy/asset body reads | +| `crates/trusted-server-core/src/publisher.rs` | 4 sinks (46 prod; 748, 1079, 1562 test) | publisher body reads | +| `crates/trusted-server-core/src/auction/endpoints.rs` | 1 sink (81 prod) | auction body read | +| `crates/trusted-server-core/src/auction/formats.rs` | 1 sink (444 test) | auction test helper | +| `crates/trusted-server-core/src/request_signing/endpoints.rs` | 4 sinks (103, 246, 365 prod; 464 test) | signing endpoint body reads | +| `crates/trusted-server-core/src/integrations/prebid.rs` | 1 sink (2067 test) | prebid test | +| `crates/trusted-server-core/src/integrations/testlight.rs` | 1 sink (461 test) | testlight test | + +`http_util.rs:456` (`enforce_max_body_size(bytes: &[u8], …)`) is **not** a sink — no edit. + +--- + +## Fix shapes (apply the matching one at each sink) + +```rust +// Shape A — value consumed directly (body_as_reader; let body = …into_bytes()) +let body = resp.into_body().into_bytes() + .expect("should have a buffered body"); + +// Shape B — chained .to_vec() (String::from_utf8(…into_bytes().to_vec())) +String::from_utf8( + resp.into_body().into_bytes() + .expect("should have a buffered body") + .to_vec(), +) + +// Shape C — bound, then borrowed into &[u8]/&Bytes (enforce_max_body_size(&b)/from_slice(&b)) +let b = req.into_body().into_bytes() + .expect("should have a buffered request body"); +enforce_max_body_size(&b, …)?; +serde_json::from_slice(&b)?; +``` + +`cargo fmt` will rewrap; write the one-liner and let it format. + +--- + +## Task 0: Create the dedicated branch off PR14 + +**Files:** none (git only) + +- [ ] **Step 1: Branch off PR14 (not in-place, not main)** + +```bash +git fetch origin +git checkout -b feature/edgezero-269-repin feature/edgezero-pr14-entry-point-dual-path +``` + +- [ ] **Step 2: Confirm base + capture the authoritative "from" pin** + +Run: `git log -1 --format='%s' && grep -m1 'edgezero-core' Cargo.toml` +Expected: PR14 tip; the dep line prints the **base pin = `rev = "38198f9…"`**. + +> Pin clarity: the Goal's `170b74b` is the _stack's original_ pin (PR1–13), **not +> this branch's base.** PR14's base is `38198f9` (spec §11). The **only** +> authoritative "from" value is whatever this grep prints — use that, not a +> hardcoded sha, if the branch has advanced. + +--- + +## Task 1: Repin edgezero to #269 + regenerate root lock + +**Files:** Modify `Cargo.toml:59-62`, regenerate `Cargo.lock` + +- [ ] **Step 1: Repin all 4 deps** + +Replace the base pin captured in Task 0 Step 2 (`rev = "38198f9…"` on the 4 +`edgezero-*` lines) with `rev = "2eeccc9748daba92b9adf6afe4df105e79269ae9"` +(#269 HEAD). (After #269 merges, use the edgezero `main` sha instead — see spec §9.) + +- [ ] **Step 2: Resolve the lock FIRST — separate resolution failure from compile-RED** + +Run: `cargo generate-lockfile` +Expected: lock resolves with no error. **If this fails** (MSRV / feature +unification / `spin-sdk` graph — the spec's #1 transitive risk, §5.1/§10), STOP +and surface it: that is a _resolution_ break, not the expected `Body` compile-RED, +and must be triaged before continuing. + +- [ ] **Step 3: Capture the RED baseline (build only after the lock resolves)** + +Run: `cargo build --workspace --all-targets 2>/tmp/ez_red.log; grep -cE '^error' /tmp/ez_red.log` +Expected: ~27 errors (`E0308`/`E0599`/`E0624`). This is the RED state. (Log goes to +`/tmp` — keep build artifacts out of the repo tree.) + +- [ ] **Step 4: Sanity — every error is at a known `Body` sink file, nothing else** + +Filter by **location**, not error-kind (an unexpected break could share a kind): + +Run: + +```bash +grep -A1 '^error' /tmp/ez_red.log | grep -- '-->' \ + | grep -vE 'trusted-server-core/src/(proxy|publisher|auction/(endpoints|formats)|request_signing/endpoints|integrations/(prebid|testlight)|http_util)\.rs' \ + && echo "UNEXPECTED — stop & investigate" || echo OK +``` + +Expected: `OK` (every error points into a known sink file from §2). Any other +location ⇒ unexpected transitive break; STOP and surface it (spec §5). + +- [ ] **Step 5: Commit the repin (still RED — that's expected)** + +```bash +git add Cargo.toml Cargo.lock +git commit -m "Repin edgezero to the extensible-cli branch (stackpop/edgezero PR 269)" +``` + +--- + +## Task 2: Fix production sinks — `proxy.rs` + +**Files:** Modify `crates/trusted-server-core/src/proxy.rs` (sinks 38, 1550, 1665) + +- [ ] **Step 1: Confirm current errors (RED)** + +Run: `cargo check --workspace 2>&1 | grep 'proxy.rs'` +Expected: errors **within `proxy.rs`** (exact line numbers vary per branch — re-derive +from this output; do not trust the §2 PR14 numbers after PR14). Expect a `body_as_reader` +`Cursor::new(body.into_bytes())` site plus two POST-body `let body_bytes = req.into_body().into_bytes();` sites. + +- [ ] **Step 2: Fix `body_as_reader` (Shape A)** + +`Cursor::new(body.into_bytes())` → `Cursor::new(body.into_bytes().expect("should have a buffered body"))` + +- [ ] **Step 3: Fix the two POST-body bindings (Shape C)** + +`let body_bytes = req.into_body().into_bytes();` → +`let body_bytes = req.into_body().into_bytes().expect("should have a buffered request body");` + +> `replace_all` only if the two lines are byte-identical (same indentation). **Read +> each site first**; if the replace count ≠ 2, fall back to per-site edits. Both are +> production code (not test). + +- [ ] **Step 4: Verify proxy.rs errors cleared** + +Run: `cargo check --workspace 2>&1 | grep -c 'proxy.rs' ; echo done` +Expected: `0` proxy.rs errors (other files may still error — fine). + +--- + +## Task 3: Fix production sinks — `publisher.rs` + `auction/endpoints.rs` + +**Files:** Modify `publisher.rs` (line 46), `auction/endpoints.rs` (line 81) + +- [ ] **Step 1: Fix `publisher.rs:46` `body_as_reader` (Shape A)** + +`std::io::Cursor::new(body.into_bytes())` → `std::io::Cursor::new(body.into_bytes().expect("should have a buffered body"))` + +- [ ] **Step 2: Fix `auction/endpoints.rs:81` (Shape C)** + +`let body_bytes = body.into_bytes();` → `let body_bytes = body.into_bytes().expect("should have a buffered request body");` + +- [ ] **Step 3: Verify both cleared** + +Run: `cargo check --workspace 2>&1 | grep -cE 'publisher.rs|auction/endpoints.rs'` +Expected: `0`. + +--- + +## Task 4: Fix production sinks — `request_signing/endpoints.rs` + +**Files:** Modify `request_signing/endpoints.rs` (lines 103, 246, 365) + +- [ ] **Step 1: Fix all three `req.into_body()` bindings (Shape C)** + +`replace_all` of ` let body = req.into_body().into_bytes();` → + +```rust + let body = req + .into_body() + .into_bytes() + .expect("should have a buffered request body"); +``` + +> Expect 3 identical occurrences, all production. **Read the sites first**; if the +> replace count ≠ 3 (indentation differs), fall back to per-site edits. The +> `json_response(body: String)` site (`String::into_bytes`) is a false positive — +> **do not touch** (verify by checking the receiver is `String`, not `Body`). + +- [ ] **Step 2: Verify lib/bin build is GREEN** + +Run: `cargo build --workspace` +Expected: **success** (all 8 production sinks fixed). Tests still red — next. + +- [ ] **Step 3: Commit production fixes** + +```bash +git add crates/trusted-server-core/src +git commit -m "Adapt Body::into_bytes Option return at production sinks" +``` + +--- + +## Task 5: Fix test sinks + +**Files:** Modify `proxy.rs` (2034, 2795, 2851), `publisher.rs` (748, 1079, 1562), `auction/formats.rs` (444), `request_signing/endpoints.rs` (464), `integrations/prebid.rs` (2067), `integrations/testlight.rs` (461) + +- [ ] **Step 1: Confirm test errors (RED)** + +Run: `cargo build --workspace --all-targets 2>&1 | grep -E '^error' | wc -l` +Expected: ~12 remaining errors, all in test code at the lines above. + +- [ ] **Step 2: Apply the matching shape at each test sink** + +- `String::from_utf8(… .into_bytes().to_vec())` → Shape B (`.expect("should have a buffered body")` before `.to_vec()`): `proxy.rs:2034`, `publisher.rs:748`, `prebid.rs:2067`, `request_signing/endpoints.rs:464`. +- `serde_json::from_slice(&… .into_bytes())` → Shape C (bind, `.expect`, borrow): `auction/formats.rs:444`, `testlight.rs:461`. +- `let x = … .into_bytes();` → Shape A (`.expect`): `proxy.rs:2795`, `proxy.rs:2851`, `publisher.rs:1079`, `publisher.rs:1562`. + +(The `request_signing/endpoints.rs:452` test helper is `str::as_bytes` — false positive, **do not touch**.) + +- [ ] **Step 3: Verify `--all-targets` is GREEN** + +Run: `cargo build --workspace --all-targets` +Expected: **success** — all 18 sinks fixed. + +- [ ] **Step 4: Commit test fixes** + +```bash +git add crates/trusted-server-core/src +git commit -m "Adapt Body::into_bytes Option return in tests" +``` + +--- + +## Task 6: Drop never-compiled adapter deps + +**Files:** Modify `Cargo.toml` (remove `edgezero-adapter-axum`, `edgezero-adapter-cloudflare` from `[workspace.dependencies]`) + +- [ ] **Step 1: Confirm they are absent from the graph** + +Run: `cargo tree -i edgezero-adapter-axum; cargo tree -i edgezero-adapter-cloudflare` +Expected: both → "did not match any packages" (no member uses them — spec §1). + +- [ ] **Step 2: Remove the two lines from `Cargo.toml` `[workspace.dependencies]`** + +Delete the `edgezero-adapter-axum = …` and `edgezero-adapter-cloudflare = …` lines (keep `edgezero-adapter-fastly` and `edgezero-core`). + +- [ ] **Step 3: Verify still builds** + +Run: `cargo build --workspace --all-targets` +Expected: success (nothing referenced them). + +- [ ] **Step 4: Commit** + +```bash +git add Cargo.toml Cargo.lock +git commit -m "Drop unused edgezero axum/cloudflare workspace deps" +``` + +> If a future `trusted-server-adapter-{axum,cloudflare}` consumer lands, re-add then. Skip this task if the team prefers to keep them pinned for symmetry — note the decision. + +--- + +## Task 7: Reconcile the integration-tests lockfile + +> **Ordering matters.** This runs _after_ all root dependency changes (repin Task 1 +> +> - drop-adapters Task 6) and _after_ `trusted-server-core` is GREEN (Tasks 2–5). +> `crates/integration-tests` is a **separate workspace** that path-deps +> `trusted-server-core` (`Cargo.toml:13`); building it any earlier fails on the +> Body errors, not lock drift — confounding the check. + +**Files:** `crates/integration-tests/Cargo.lock` (and `crates/openrtb-codegen/Cargo.lock` if it drifts) + +- [ ] **Step 1: Resolve + build the integration-tests workspace** + +Run: `( cd crates/integration-tests && cargo generate-lockfile && cargo build --workspace 2>&1 | tail -20 )` +Expected: lock resolves and it builds. Because core is now green, any failure here +is a _real_ signal — shared-dep drift or a genuine break — not the Body RED. + +- [ ] **Step 2: If shared-dep drift, reconcile with targeted updates only** + +For each mismatched shared dep (e.g. `bytes`, `http`, `serde`): +Run: `( cd crates/integration-tests && cargo update -p --precise )` +**Never** a blanket `cargo update`. (Project CI gate: shared direct deps must match +root.) Repeat in `crates/openrtb-codegen` if it drifted. + +- [ ] **Step 3: Verify** + +Run: `( cd crates/integration-tests && cargo build --workspace )` +Expected: success. + +- [ ] **Step 4: Commit if changed** + +```bash +git add crates/integration-tests/Cargo.lock crates/openrtb-codegen/Cargo.lock +git commit -m "Reconcile integration-tests lockfile after edgezero repin" || echo "nothing to commit" +``` + +--- + +## Task 8: Full verification gate + +**Files:** none (verification only) + +- [ ] **Step 1: Compile gate (host + all-targets)** + +Run: `cargo build --workspace --all-targets` +Expected: success. + +- [ ] **Step 2: wasm32-wasip1 (Fastly deploy target)** + +Run: `cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1` +Expected: success. (First leg not yet verified at spec-freeze — this is the gate that proves it.) + +- [ ] **Step 3: Tests** + +Run: `cargo test --workspace` +Expected: pass. Watch for behavioral diffs in body-handling tests (they exercise the `.expect()` paths). + +- [ ] **Step 4: Clippy + fmt** + +Run: `cargo clippy --workspace --all-targets --all-features -- -D warnings && cargo fmt --all -- --check` +Expected: clean. + +- [ ] **Step 5: integration-tests + JS gates (per CLAUDE.md CI)** + +Lock already reconciled (Task 7) — this just runs the suites. +Run: `( cd crates/integration-tests && cargo test --workspace )` and `( cd crates/js/lib && npx vitest run )` +Expected: pass (JS untouched — sanity only). + +- [ ] **Step 6: Commit any fmt fixups** + +```bash +git add crates Cargo.toml Cargo.lock && git commit -m "Verification gate fixups" || echo "nothing to commit" +``` + +(Scope the add — never `git add -A`, which would sweep stray build logs into the commit.) + +--- + +## Task 9: Open the PR (PR14-based dedicated branch) + +**Files:** none + +- [ ] **Step 1: Push + open PR targeting the PR14 branch (or wherever the stack lands)** + +```bash +git push -u origin feature/edgezero-269-repin +gh pr create --base feature/edgezero-pr14-entry-point-dual-path \ + --title "Repin edgezero to #269 and adapt Body::into_bytes Option return" \ + --body "See docs/superpowers/specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md" +``` + +> Do not push or open the PR until the user approves (per project git rules). Confirm the base branch with the team — the stack tip may have advanced. + +--- + +## Task 10: Propagate up the stack (merge, not rebase) + +**Files:** none (per-branch merge + gate) + +> **Approval gate:** merging up mutates 6 review branches. Do **not** run this task +> (or any `git push`) until the user approves — same rule as Task 9. + +For each branch PR15 → PR16 → PR17 → PR18 → PR19 → PR20, in order: + +- [ ] **Step 1: Merge the repin forward** + +```bash +git checkout feature/edgezero-pr15-remove-fastly-core +git merge feature/edgezero-269-repin # merge, not rebase (team preference) +``` + +- [ ] **Step 2: Re-derive sinks from the compiler (line numbers/sink set shift per layer)** + +Run: `cargo build --workspace --all-targets 2>&1 | grep -E 'into_bytes|Body'` +Resolve any new/moved sinks with the §2 fix shapes. PR15 (remove-fastly-core) and PR16+ move/delete these files — expect manual conflict resolution, not clean fast-forward. + +- [ ] **Step 3: Run the full gate (Task 8) on this branch** + +Expected: green before moving to the next branch up. + +- [ ] **Step 4: Commit the merge resolution (if any)** + +```bash +git add -A && git commit --no-edit || echo "nothing to commit (clean fast-forward)" +``` + +Repeat for each branch up to PR20. + +--- + +## Follow-on plans (NOT this plan) + +Per spec §6/§9, after the repin lands and #269 merges to edgezero `main`: + +1. **Re-pin to `main`** (one-line dep change + gate). +2. **Store convergence** (spec §6 B): map `PlatformConfigStore`/`SecretStore` reads onto edgezero `ConfigStore`/`SecretStore`/`StoreRegistry` + thin write-CRUD extension. +3. **Typed `AppConfig` (two-tier)** + **CLI port** (spec §6 C / §7) — Christian. Shared contract = the config struct + `[stores.config]` id; agree before either starts. + +Each gets its own spec → plan cycle. diff --git a/docs/superpowers/specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md b/docs/superpowers/specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md new file mode 100644 index 00000000..35b6a089 --- /dev/null +++ b/docs/superpowers/specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md @@ -0,0 +1,557 @@ +# Finding: edgezero #269 repin — breaking-API surface for trusted-server + +- **Date:** 2026-06-16 (build-verified 2026-06-17, §10) +- **Author:** Prakash (HTTP port); Christian owns the CLI port (see §7) +- **Upstream:** [stackpop/edgezero#269](https://github.com/stackpop/edgezero/pull/269) + "EdgeZero CLI Extensions" — head `feature/extensible-cli`, base `main`, + **OPEN / unmerged** as of 2026-06-16. Sibling adaptation precedent: + [stackpop/mocktioneer#110](https://github.com/stackpop/mocktioneer/pull/110) + (design + plan only, docs-only). +- **Companion doc:** extends + [2026-03-19-edgezero-migration-design.md](./2026-03-19-edgezero-migration-design.md) + (the original Fastly→EdgeZero migration, PRs 1–17; PR13 merged, branch now on + PR19 canary cutover). +- **Method:** diffed the exact pin `170b74b` (current) against + `feature/extensible-cli` HEAD (`git diff 170b74b..HEAD`, 257 commits, + +28,969 / −5,754 across 143 files), then cross-referenced every changed + public symbol against trusted-server's actual consumption sites. All + signatures below are quoted verbatim from the two refs; call sites are + `file:line` in this repo. + +--- + +## 0. TL;DR + +1. **trusted-server currently pins edgezero at `170b74b`** (March 2026, "unified + key-value store abstraction #165"). `feature/extensible-cli` is **257 commits + ahead** of that exact commit — and `170b74b` is a clean ancestor, so a repin + to post-#269 swallows the **entire** delta, not just #269's own commits. + +2. **Almost none of #269's headline breaks reach trusted-server.** The original + migration deliberately wrapped edgezero behind trusted-server's own + `platform/` trait layer (`RuntimeServices`, `PlatformConfigStore`, + `PlatformSecretStore`, `PlatformKvStore`, `PlatformHttpClient`). As a result + trusted-server uses **none** of: `run_app`, the `app!` macro, + `RequestContext`, edgezero extractors, typed `AppConfig`, or `[stores.*]` + manifest tables. Every one of those is where #269's breakage lives. + +3. **The actual code-level break is a single method:** + `edgezero_core::body::Body::into_bytes()` now returns `Option` instead + of panicking. (`as_bytes()` changed the same way but has **no** trusted-server + call site — §2.) **Compiler-enumerated** (not rg-guessed): **18 sink bindings + — 8 production + 10 test-only** (§2/§10). Mechanical fix. + +4. **`KvError` going `#[non_exhaustive]` + two new variants does _not_ break us** + — trusted-server only _constructs_ `KvError::Unavailable` and never + exhaustively matches the enum. + +5. **Strategic question for the HTTP port:** #269 matures edgezero's _own_ + first-class multi-store registry, async `ConfigStore`/`SecretStore`, + `Config`/`Secrets`/`Kv` extractors, and typed `AppConfig`. These now overlap + heavily with trusted-server's bespoke `platform/` layer. The HTTP port can be + a **minimal repin** (keep the bespoke layer) or a **convergence** onto + edgezero's surface. See §6. + +6. **The stack already walks edgezero forward** — pins are _not_ frozen at + `170b74b`: PR1–13 = `170b74b`, PR14–18 = `38198f9`, PR19–20 = `ce6bcf7`. The + #269 repin is the next step of a bump the team already does. PR14 is where + trusted-server _starts_ consuming edgezero's high-level surface + (`RequestContext`/`EdgeError`/middleware/router) — yet a real build (§10) + proves even that base breaks on **nothing but `Body`**. See §11. + +7. **Plan (agreed):** do the upgrade on a **dedicated branch off PR14** (not on + main, not in-place on any reviewed PR), then **merge up** the stack; **re-pin + to edgezero `main` after #269 merges**. Full-adaptation roadmap (store + convergence + typed config + entry-point) is a _separate, optional_ track — + **not** forced by the repin (§11). + +--- + +## 1. What trusted-server actually consumes from edgezero + +Verified by `rg` across `crates/`. The dependency is a **thin, low-level slice** +of `edgezero-core` plus one type from `edgezero-adapter-fastly`. + +| edgezero symbol | trusted-server usage | Reaches #269 break? | +| ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| `edgezero_core::body::Body` (alias `EdgeBody`) | pervasive — bodies on every request/response, integrations, publisher, auction | **YES** (`into_bytes` → `Option`; `as_bytes` changed too but has **no** TS sink) | +| `edgezero_core::http::{Request, Response, request_builder, response_builder, HeaderValue, …}` | request/response construction in `platform.rs`, `proxy.rs`, `http_util.rs`, tests | No (stable; alias names unchanged) | +| `edgezero_core::key_value_store::{KvStore (as `PlatformKvStore`), KvError, KvHandle, KvPage, NoopKvStore}` | KV trait impls, EC identity graph, `UnavailableKvStore` stub | No (trait sigs unchanged; `KvError` change is inert for us — §3.2) | +| `edgezero_adapter_fastly::key_value_store::FastlyKvStore` | `platform.rs:13` — only symbol used from the fastly adapter | No (`open()` sig unchanged) | +| `edgezero-adapter-axum`, `edgezero-adapter-cloudflare` (workspace deps) | **declared in root `[workspace.dependencies]` only; no member crate references them — `cargo tree -i edgezero-adapter-axum` / `-cloudflare` return "did not match any packages"** | No (absent from the dependency graph — not compiled at all; see §9 Q4 → drop) | +| `edgezero-adapter-spin` | **not a dependency** — `trusted-server-adapter-spin` is an in-repo stub | No (edgezero's Spin SDK6/wasip2 churn never reaches us) | + +**Not used at all** (and therefore immune to #269): `run_app`, `app!`, +`RequestContext`, `FromRequest`/extractors, `EdgeError`, `IntoResponse`, +`ProxyClient`/edgezero `proxy`, typed `AppConfig`, manifest `[stores.*]` / +`[adapters.*]` tables. trusted-server's manifest is `trusted-server.toml` (a +bespoke `Settings` struct in `settings.rs`), **not** an edgezero `edgezero.toml`. + +--- + +## 2. The one break that reaches trusted-server: `Body` → `Option` + +`crates/edgezero-core/src/body.rs`. The `Body` enum shape is **unchanged** — +`Body::Once(Bytes)` / `Body::Stream(LocalBoxStream<…>)` — so trusted-server's +pattern matches in `platform.rs` survive. Two accessor return types changed: + +| Item | BASE (`170b74b`) | HEAD (`feature/extensible-cli`) | Break | +| ------------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------- | +| `Body::as_bytes` | `pub fn as_bytes(&self) -> &[u8]` (panics on `Stream`) `body.rs:48` | `pub fn as_bytes(&self) -> Option<&[u8]>` (`None` on `Stream`) `body.rs:24` | return type | +| `Body::into_bytes` | `pub fn into_bytes(self) -> Bytes` (panics on `Stream`) `body.rs:55` | `pub fn into_bytes(self) -> Option` (`None` on `Stream`) `body.rs:62` | return type | + +Everything else on `Body` is intact: `empty()`, `from_bytes()`, `from_stream()`, +`into_stream()`, `is_stream()`, `text()`, `json()`, `to_json()`, and `stream()` +(the earlier subagent claim that `stream()` was removed is **wrong** — it moved +in source order only). `From` impls unchanged. + +**Behavior** (not just signatures) verified for the accessor neighbours: `Body::to_json` +is byte-identical BASE↔HEAD and matches on the `Once`/`Stream` enum directly — +it never calls `as_bytes`, so the `Option`-return change cannot leak into JSON +deserialization. `text()`/`json()`/`to_json()` are unused in trusted-server +regardless. + +### Affected call sites — compiler-enumerated (authoritative) + +> **Source of truth = the compiler, not `rg`.** An earlier hand-built `rg` list +> was wrong (missed production sinks `proxy.rs:38` and `auction/endpoints.rs:81`; +> mis-tagged tests as production). The list below is the exhaustive set from +> `cargo build --workspace --all-targets` on the repinned spike (§10): **27 +> compiler errors collapsing to 18 distinct `into_bytes()` sink bindings.** All +> are `Body` (`EdgeBody`); **no `as_bytes` site exists** in trusted-server. + +Line numbers are **PR14-base**; they shift per branch as the stack rewrites these +files — re-derive from the compiler on whatever branch you repin (the §8 gate does +this). One binding often produces several errors (`.len()`, `.to_vec()`, +`from_slice(&…)`, `from_utf8(&…)` on the now-`Option`). + +**Production (8) — fail plain `cargo build` (lib + bin):** + +| Binding site (PR14) | Shape | +| ------------------------------------ | ------------------------------------------------------------------- | +| `proxy.rs:38` (`body_as_reader`) | `Cursor::new(body.into_bytes())` | +| `publisher.rs:46` (`body_as_reader`) | `Cursor::new(body.into_bytes())` | +| `auction/endpoints.rs:81` | `let b = body.into_bytes(); b.len(); from_slice(&b)` | +| `proxy.rs:1550` | `let b = req.into_body().into_bytes(); enforce(&b); from_utf8(&b)` | +| `proxy.rs:1665` | same shape (rebuild path) | +| `request_signing/endpoints.rs:103` | `let b = req.into_body().into_bytes(); enforce(&b); from_slice(&b)` | +| `request_signing/endpoints.rs:246` | same (rotate; also `b.is_empty()`) | +| `request_signing/endpoints.rs:365` | same (deactivate) | + +**Test-only (10) — fail only `cargo test` / `--all-targets`, invisible to plain +`cargo build`:** + +`auction/formats.rs:444`, `integrations/prebid.rs:2067`, +`integrations/testlight.rs:461`, `proxy.rs:2034`, `proxy.rs:2795`, +`proxy.rs:2851`, `publisher.rs:748`, `publisher.rs:1079`, `publisher.rs:1562`, +`request_signing/endpoints.rs:464`. + +**NOT a sink:** `http_util.rs:456` appears in errors as the _expected_ side — it's +the `enforce_max_body_size(bytes: &[u8], …)` signature. No edit; `&Bytes` derefs +to `&[u8]` once the caller unwraps. + +**False positives — leave untouched** (receiver is `str`/`String`/`FromUtf8Error`, +not `Body`; confirmed by source): +`http_util.rs:286,320` (`str::as_bytes`), `request_signing/endpoints.rs:23` +(`String::into_bytes`), `request_signing/endpoints.rs:452` (test, `str::as_bytes`), +`sourcepoint.rs:571` / `datadome.rs:323` (`rewrite_script_content() -> String`), +`sourcepoint.rs:822` (`FromUtf8Error::into_bytes`). + +### Fix — three distinct shapes + +All sinks are buffered (`Once`) bodies, so use an explicit `.expect()` with a +`should`-style message (per CLAUDE.md), **not** `unwrap_or_default()`. If a future +sink is genuinely streaming, branch on `None` / use `into_stream()`. + +```rust +// Shape A — value consumed directly (e.g. proxy.rs:38, publisher.rs:46) +let body = resp.into_body().into_bytes() + .expect("should have a buffered body"); + +// Shape B — chained .to_vec() (e.g. prebid.rs:2067, proxy.rs:2034) +String::from_utf8( + resp.into_body().into_bytes() + .expect("should have a buffered body") + .to_vec(), +) + +// Shape C — bound, then borrowed into &[u8] / &Bytes (e.g. proxy.rs:1550, +// auction/endpoints.rs:81, request_signing/endpoints.rs:*) +let b = req.into_body().into_bytes() + .expect("should have a buffered request body"); +enforce_max_body_size(&b, …)?; // &Bytes → &[u8] +serde_json::from_slice(&b)?; // borrow the unwrapped Bytes +``` + +No signature changes propagate to callers (the locals are consumed in place). + +--- + +## 3. Low-level surface that changed upstream but is INERT for trusted-server + +These changed across the 257-commit delta but do **not** break our build, +verified against actual usage. Documented so the repin reviewer doesn't chase +ghosts. + +### 3.1 `KvStore` trait — reordered, not re-signed + +`key_value_store.rs`. All methods keep identical signatures +(`get_bytes`/`put_bytes`/`put_bytes_with_ttl`/`delete`/`list_keys_page`/`exists`, +all `async`, same params/returns). Only source ordering changed (clippy +`arbitrary_source_item_ordering`). trusted-server's `UnavailableKvStore` / +`NoopKvStore` impls and `KvIdentityGraph` calls are unaffected. + +### 3.2 `KvError` — `#[non_exhaustive]` + new variants (inert here) + +| | BASE | HEAD | +| -------- | ------------------------------------ | --------------------------------------------------------------------------- | +| attr | (none) | `#[non_exhaustive]` `key_value_store.rs:302` | +| variants | `NotFound { key }`, `Unavailable`, … | adds `LimitExceeded { message }` `:311`, `Unsupported { operation }` `:328` | + +Inert for us: trusted-server only **constructs** `KvError::Unavailable` (a unit +variant — still constructible downstream under enum-level `#[non_exhaustive]`) in +`platform/kv.rs`, and never writes an exhaustive `match` on `KvError`. No catch-arm +needed. + +### 3.3 `KvPage`, `KvHandle`, `NoopKvStore`, `FastlyKvStore::open`, http builders + +All present and signature-stable: + +- `KvPage` — same fields (`keys`, `cursor`/etc.), alphabetized only. +- `KvHandle` `key_value_store.rs:354`, `NoopKvStore` `:818` — exist. +- `edgezero_core::http::request_builder()` / `response_builder()` — same + signatures; `RequestBuilder`/`ResponseBuilder` are still exported alias names + (now aliasing `HttpRequestBuilder`/`HttpResponseBuilder` internally, transparent + to us); `Request`/`Response`/`Method`/`StatusCode`/`HeaderValue`/… aliases + unchanged. +- `edgezero_adapter_fastly::key_value_store::FastlyKvStore::open(name: &str) -> +Result` — unchanged. + +--- + +## 4. Full #269 breaking-API catalog (does NOT reach trusted-server today) + +Recorded for completeness — these are the framework-level breaks that bite +_consumers who use edgezero's high-level surface_ (e.g. mocktioneer). They matter +to us only **if** the HTTP port chooses convergence (§6) or when Christian's CLI +port (§7) adopts typed config. Grouped by subsystem. + +### 4.1 Adapter entrypoints — `run_app` dropped the manifest arg + +| Adapter | BASE | HEAD | +| ---------- | -------------------------------------------------------------------- | -------------------------------------------------- | +| axum | `pub fn run_app(manifest_src: &str) -> anyhow::Result<()>` | `pub fn run_app() -> anyhow::Result<()>` | +| cloudflare | `run_app(manifest_src: &str, req, env, ctx)` | `run_app(req, env, ctx)` | +| fastly | `run_app(manifest_src: &str, req)` | `run_app(req)` | + +Manifest/store config now flows from `A::stores()` (macro-baked) + `EDGEZERO__*` +env vars instead of an `include_str!` manifest string. **Not used by us** (we have +a manual `fn main()` event loop, not `run_app`). + +### 4.2 `Hooks::stores()` + `StoresMetadata` + +New `fn stores() -> StoresMetadata` on the `Hooks` trait (default impl returns +empty). The `app!` macro auto-emits it from `[stores.*]`. **Not used by us** (no +`app!`, no `Hooks` impl). + +### 4.3 Manifest `[stores.*]` hard rewrite + +Old per-adapter shape is now a **hard load error**: + +```toml +# BASE (now rejected) +[stores.kv] +name = "MY_KV" +[stores.kv.adapters.cloudflare] +name = "CF_BINDING" + +# HEAD (portable; names move to env) +[stores.kv] +ids = ["default"] +default = "default" +``` + +`[adapters..stores.*]` and unknown `[adapters..*]` subtables now +fail `manifest.validate()`. `Manifest::kv_store_name()` removed → runtime +`EnvConfig::store_name(kind, id)` (`EDGEZERO__STORES______NAME`). +**Not used by us** (no edgezero manifest). + +### 4.4 Multi-store registry + async `ConfigStore`/`SecretStore` + +New `store_registry.rs`: `StoreRegistry` with `default()`/`named(id)`, aliases +`KvRegistry`/`ConfigRegistry`/`SecretRegistry`, and `BoundSecretStore` (binds +platform store name per logical id). New async read traits +`ConfigStore::get(&self, key) -> Result, ConfigStoreError>` and +`SecretStore::get_bytes(&self, store_name, key) -> Result, …>`. +**Overlaps directly with our `PlatformConfigStore`/`PlatformSecretStore`** — see +§6. Not consumed today. + +### 4.5 `RequestContext` store accessors + +`kv_handle()` removed; replaced by `kv_store(id)`/`kv_store_default()` (+ config +& secret variants) returning `Option`. **Not used by us** (no +`RequestContext`). + +### 4.6 Extractor overhaul + +`Kv(KvHandle)` → `Kv(KvRegistry)` with `.default()`/`.named(id)`; new +`Config`/`Secrets` extractors. **Not used by us.** + +### 4.7 `#[derive(AppConfig)]` + `#[secret]` (entirely new) + +New derive macro (`edgezero-macros/src/app_config.rs`) emitting `AppConfigMeta` +with `SECRET_FIELDS`. `#[secret]` / `#[secret(store_ref)]` only on scalar +`String` fields; rejects `Option`, `Cow`, non-scalars, `serde(rename)`, +container `rename_all`, duplicate/`=`/unknown-arg forms (compile-fail UI tests). +Pairs with CLI `config validate`/`config push`. **This is Christian's CLI-port +territory** (§7); net-new adoption, not a break. + +### 4.8 `EdgeError` / `IntoResponse` / `ProxyResponse` + +`EdgeError` now `#[non_exhaustive]`, gains `NotImplemented`, `source()`→`inner()`. +`IntoResponse::into_response` now returns `Result`. +`ProxyResponse::into_response` returns `Result`. **None used by us** (we use +`error_stack::Report` and never touch `EdgeError`/edgezero +`IntoResponse`/edgezero `proxy`). + +### 4.9 CLI surface (new) — inventory only + +New `edgezero-cli` commands: `auth` (login/logout/status), `provision`, +`config validate`, `config push`, `demo` (replaces `dev`). Generated `-cli` +crate per app. Typed entrypoints a consumer crate calls: +`run_config_validate_typed::()`, `run_config_push_typed::()`, plus +`run_{auth,build,deploy,provision,serve}`. **Christian's port** (§7). + +### 4.10 Spin adapter — SDK 6.0 / wasip2 + +edgezero's Spin adapter moved to `spin-sdk ~6.0`, `wasm32-wasip1`→`wasip2`, +`#[http_component]`→`#[http_service]`, `IncomingRequest`→`Request`, async stores. +**Does not reach us** — trusted-server does not depend on `edgezero-adapter-spin`; +our `trusted-server-adapter-spin` is an in-repo stub. + +--- + +## 5. Net repin work for the HTTP port (minimal path) + +If the HTTP port is a **straight repin** (keep the bespoke `platform/` layer): + +1. Bump the four `edgezero-*` git deps in root `Cargo.toml` from `rev = "170b74b"` + to the #269 branch (then to `main` post-merge), regenerate root `Cargo.lock`. +2. **Reconcile `crates/integration-tests/Cargo.lock`** (it has its own lock; so + does `crates/openrtb-codegen/`). CI enforces that shared direct deps match + between the root and integration-tests lockfiles, and the 257-commit edgezero + delta will drag shared transitive deps (bytes/http/serde/…). Fix with targeted + `cargo update -p --precise ` in the integration-tests workspace — + **never** a blanket `cargo update`. +3. Fix the **18 `Body::into_bytes()` sink bindings** (§2) — 8 production + 10 + test — with explicit `.expect("should …")` / `None` handling. +4. Run the full gate (§8): host + Fastly `wasm32-wasip1`, **`--all-targets`**, + clippy `-D warnings`, `cargo test --workspace`, `cargo fmt --check`. + +**Status:** compilation is now **verified** (host, lib + tests — §10): the forced +code delta is the `Body` sinks and nothing else. **Still unverified:** +`wasm32-wasip1`, clippy, full test pass, and lockfile reconciliation (step 2). +Source-API diffing cannot see transitive breaks (dep bumps, MSRV, feature +unification, `spin-sdk ~6.0` lock entries); the §8 matrix is the proof, not this +document. "Does not reach us" (§4.10) is true at _source_ level but does not by +itself prove the lock graph resolves or that wasm builds. + +### 5.1 Risks & assumptions + +| Risk / assumption | Mitigation | +| ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Pinning to an **OPEN, unmerged, force-pushable** ref (`feature/extensible-cli`) | Pin to the branch only if we must move pre-merge; re-pin to edgezero `main` after #269 merges (mirrors mocktioneer #110). | +| **Transitive build breakage** unseen by source diffing (lock re-resolution, MSRV, features, spin-sdk 6) | Verification gate above — actually build on a scratch branch before sign-off. | +| Branch rebased / dep destabilizes after we pin | **Rollback = single-commit revert.** `170b74b` stays recoverable; the repin is one `Cargo.toml`/`Cargo.lock` commit — `git revert` it to return to the known-good pin. | +| Assumption: all 18 `Body` sinks are buffered (`Once`) bodies | True today (compiler-confirmed receivers); if a future sink is genuinely streaming, use `into_stream()` / branch on `None` instead of `.expect()`. | +| **integration-tests lockfile drift** — CI fails if shared direct deps diverge between root and `crates/integration-tests/Cargo.lock` | Reconcile with targeted `cargo update -p --precise` (§5 step 2); never blanket-update. | +| **Test-only sinks slip through** a `cargo build`-only check | Gate runs `--all-targets` + `cargo test` (§8) — 10 of 18 sinks are test-only (§10). | + +--- + +## 6. Strategic divergence (decision for the HTTP port) + +The original migration built a trusted-server-owned abstraction: +`RuntimeServices { config_store, secret_store, kv_store, backend, http_client, +geo, client_info }` with `PlatformConfigStore`/`PlatformSecretStore`(full CRUD)/ +`PlatformKvStore`/`PlatformHttpClient` traits. #269 ships edgezero's _own_ +first-class equivalents: async `ConfigStore`/`SecretStore` read traits, the +multi-store `StoreRegistry`, `Config`/`Secrets`/`Kv` extractors, env-var store +binding, and typed `AppConfig`. + +So two parallel abstractions now exist for the same job. The original design doc +already flagged this risk (PR2: "these must not coexist as parallel abstractions +… file an EdgeZero issue to generalize `ProxyClient` into `HttpClient`"). #269 is +edgezero answering that — on the store/config axis. + +| Option | What it means | Cost | Pull | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **A. Minimal repin** | Keep `platform/` layer; only fix `Body`. | 18 sinks | Fastest; preserves CRUD writes (edgezero stores are read-only) and the `select()` fan-out we depend on. | +| **B. Converge stores** | Map `PlatformConfigStore`/`SecretStore` reads onto edgezero's `ConfigStore`/`SecretStore` + `StoreRegistry`; keep our write-CRUD as an extension. | medium | Aligns with framework direction; less bespoke code. But edgezero read traits don't cover our management writes (key rotation), so the layer can't fully dissolve. | +| **C. Full adoption** | Also take `run_app`/`app!`/`RequestContext`/extractors/typed `AppConfig`. | large | Matches mocktioneer; big rewrite of our manual dispatch + `Settings`. | + +**Recommendation:** ship **A** as the repin (unblocks everything, tiny diff), +then evaluate **B** as a separate follow-up once #269 lands on `main`. **C** is a +roadmap question, not a repin question — and it's where the HTTP/CLI split +actually pays off: typed `AppConfig` (C/§4.7) is Christian's CLI surface, and our +read-store convergence (B) is the HTTP surface. The shared contract between the +two of you is **the typed config struct + its `[stores.config]` declaration** — +agree its shape and store id before either port starts. + +--- + +## 7. HTTP-port vs CLI-port split + +| | HTTP port (Prakash) | CLI port (Christian) | +| ----------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| #269 axis | runtime / stores / adapters (§4.1–4.6, 4.8, 4.10) | CLI + typed config (§4.7, 4.9) | +| repin-forced work | `Body` fix (§2) | none (trusted-server has no edgezero CLI today) | +| net-new adoption | optional store convergence (§6 B) | `-cli` crate, `auth`/`provision`/`config validate`/`config push`, `#[derive(AppConfig)]`, CI `config validate --strict` gate | +| shared seam | **reads** the typed config from the bound store | **defines/validates/pushes** the typed config | + +Note: because trusted-server uses a bespoke `Settings` + `trusted-server.toml` +(not edgezero config), Christian's CLI port is largely a **net-new adoption** +(mirroring mocktioneer #110 §3.5–3.9), not a break-fix. Sequence the typed-config +struct contract first; both ports depend on it. + +--- + +## 8. Verification commands + +```bash +# reproduce the upstream diff base (full clone; 170b74b is not in a shallow fetch) +cd /tmp && rm -rf ez && git clone https://github.com/stackpop/edgezero ez && cd ez +git fetch origin feature/extensible-cli +git diff 170b74b..origin/feature/extensible-cli -- crates/edgezero-core/src/body.rs + +# after repin, in this repo — the full gate: +cargo build --workspace --all-targets # CRITICAL: --all-targets, else 10 test-only sinks hide +cargo build --package trusted-server-adapter-fastly --target wasm32-wasip1 +cargo test --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo fmt --all -- --check + +# integration-tests lockfile gate (separate workspace lock): +( cd crates/integration-tests && cargo build --workspace ) # must resolve against root deps +``` + +Expected pre-fix failures: type errors at the 18 `Body` sinks (§2) — `Option` +has no `.len()`/`.to_vec()`, can't pass where `Bytes`/`&[u8]` expected. **8 surface +under plain `cargo build`; the other 10 only under `--all-targets`/`cargo test`.** + +Compilation green is **verified** (§10). The remaining legs — wasm, clippy, full +test, lockfile reconciliation — are the proof of "done"; transitive lock/MSRV/ +feature breaks surface only here, not in source-API diffing. **Re-run this gate on +every branch as the pin advances** (§11), since the sink set and line numbers +shift per layer. + +--- + +## 9. Decisions & open questions + +**Decided (this review cycle):** + +- **Repin target:** pin the upgrade branch to `feature/extensible-cli` (or its + HEAD sha) to start now; **re-pin to edgezero `main` after #269 merges.** +- **Where:** dedicated branch **off PR14** — _not_ main, _not_ in-place on any + reviewed PR — then **merge up** the stack (merge, not rebase). See §11. +- **Scope:** the _forced_ repin work is minimal (the 18 `Body` sinks, §2/§10). The + full A+B+C adaptation (store convergence onto edgezero's `ConfigStore`/ + `SecretStore`/`StoreRegistry`; two-tier typed `AppConfig`; entry-point + convergence) is a **separate, optional roadmap** — see §6 and the companion + full-adaptation design — and is **not** required by the repin. +- **Platform layer:** _hybrid_ — converge stores onto edgezero (+ a thin + write-CRUD extension for rotation), keep `PlatformHttpClient`/`PlatformBackend`/ + `Geo`/`ClientInfo` (edgezero gaps). Roadmap, not repin. +- **`edgezero-adapter-axum`/`cloudflare`:** drop — absent from the dependency + graph (`cargo tree -i` matches no package, §1); not compiled. + +**Still open:** + +1. Typed-config struct + `[stores.config]` id: the shared HTTP/CLI contract — + agree with Christian before either port lands (roadmap, not repin). +2. wasm32-wasip1 + clippy + test legs of the verification gate (§10 covered host + build only). + +--- + +## 10. Verified build result (spike) + +The §5 "prediction, not a result" hedge is **discharged for compilation** (host). +A throwaway branch `spike/edgezero-269-upgrade` was cut **off PR14** (base pin +`38198f9`), repinned to #269 HEAD (`2eeccc9`), and built twice: + +``` +cargo build --workspace → exit 101, 15 errors (lib + bin only) +cargo build --workspace --all-targets → exit 101, 27 errors (adds tests) +``` + +**Every error is downstream of `Body::into_bytes` → `Option`, all in +`trusted-server-core`. Zero errors from `RequestContext`, `EdgeError`, middleware, +router, or any other #269 churn — even though PR14 imports all of those.** This +empirically confirms the central thesis: the repin's forced code change is the +`Body` break and nothing else. + +The two runs differ by design and this gap is the lesson: + +- **Plain `cargo build`** compiles lib + bin → the **8 production** sinks (§2). +- **`--all-targets`** also compiles tests → **+10 test-only** sinks. These are + **invisible to plain `cargo build`** and only fail under `cargo test` / + `--all-targets`. A repin that greens `cargo build` but skips `--all-targets` + would ship a red test suite. **The gate (§8) must include `--all-targets`.** + +27 raw errors collapse to **18 distinct `into_bytes()` bindings** (one binding → +several errors via `.len()`/`.to_vec()`/`from_slice(&…)`/`from_utf8(&…)`). Full +enumeration with the production/test split is §2 — that list is now the +compiler's, superseding the earlier rg attempt (which missed `proxy.rs:38` and +`auction/endpoints.rs:81`). + +**Not yet run** (remaining gate legs): `wasm32-wasip1` build, `cargo clippy -D +warnings`, `cargo test --workspace`, and the integration-tests lockfile +reconciliation (§5.1). Compilation-green ≠ gate-green. + +> Evidence branch kept (repin only; trial code edits reverted) so the failing +> build is reproducible. No real branch was modified. + +--- + +## 11. Impact on the in-flight stacked migration branches + +The stack is `PR1 → … → PR20`, partially linear / partially diverged +(PR15/PR16/PR19 are not clean descendants of their predecessor — merges +happened). Pins climb in steps: + +| Branches | edgezero pin | date | +| ----------- | ------------ | ------ | +| PR1–13 | `170b74b` | Mar 18 | +| PR14–18 | `38198f9` | Apr 9 | +| PR19–20 | `ce6bcf7` | May 21 | +| (#269 HEAD) | `2eeccc9` | Jun 12 | + +**Key facts:** + +- `Body::into_bytes`→`Option` landed in `7ec2ad1` ("strict clippy #257", **Jun + 12**) — _after every current stack pin_ (latest is May 21). So **no existing + branch has absorbed the `Body` break**; whichever branch first bumps to a + rev ≥ Jun-12 eats all 18 sinks (§2). +- **PR14 is the inflection**: it introduces `run_app`/`RequestContext`/ + `EdgeError`/`middleware`/`router` consumption (PR13 has none). That is the + high-level surface #269 churned — _but the §10 build proves it does not break_. +- main's "only `Body`" story is therefore true **for the whole stack**, not just + main. The earlier worry that PR14+ would drag in context/error/router breakage + is **disproven by build**. + +**Why not repin at the bottom (main / `upgrade-edgezero-http-layer`):** that base +predates the consuming code, so it can't exercise PR14+ at all; the real build +signal only exists from PR14 up. + +**Why a dedicated branch off PR14, not in-place on PR14:** PR14 is still under +review; folding a version bump into it conflates two review concerns and forces +rework when review feedback rewrites the migrated code. A branch _on top of_ PR14 +gets the same build signal without disturbing any open review. + +**Propagation:** land the repin + `Body` fix once on the dedicated branch, then +**merge up** PR14→15→16→17→18→19→20 (merge, not rebase, per team preference). +Conflicts will cluster in the files every layer rewrites — `publisher.rs`, +`proxy.rs`, `request_signing/endpoints.rs`, `auction/endpoints.rs`. Run the §10 +gate (host + wasm + clippy + test) **per branch as the pin advances**, not once. From 9571f706449f1bf2119f05296e7a70abea6a4ef0 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Thu, 18 Jun 2026 13:19:37 +0530 Subject: [PATCH 2/2] Revise repin spec and plan for ts-cli-next convergence --- .../plans/2026-06-17-edgezero-269-repin.md | 494 ++++++------------ ...edgezero-269-repin-breaking-api-finding.md | 138 ++++- 2 files changed, 298 insertions(+), 334 deletions(-) diff --git a/docs/superpowers/plans/2026-06-17-edgezero-269-repin.md b/docs/superpowers/plans/2026-06-17-edgezero-269-repin.md index 43118c19..bfdff332 100644 --- a/docs/superpowers/plans/2026-06-17-edgezero-269-repin.md +++ b/docs/superpowers/plans/2026-06-17-edgezero-269-repin.md @@ -1,427 +1,267 @@ -# EdgeZero #269 Repin Implementation Plan +# EdgeZero #269 HTTP-Layer Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Repin trusted-server's edgezero dependency from its current pin (`38198f9` on the PR14 base; `170b74b` is the stack's _original_ PR1–13 pin) to post-#269 and fix the only forced code break (`Body::into_bytes` → `Option`), keeping the bespoke `platform/` layer unchanged. +**Goal:** Land the HTTP-layer (runtime) half of adopting edgezero `stackpop/edgezero#269` in trusted-server — by **converging onto Christian's `feature/ts-cli-next`** (which already carries the repin, the `Body` fixes, and runtime Settings-from-config-store for Fastly), then closing the runtime gaps it leaves: seed-before-serve safety, secrets/KV runtime wiring, non-Fastly adapters, and the missing runtime-config-store spec. -**Architecture:** Mechanical dependency bump on a dedicated branch off `feature/edgezero-pr14-entry-point-dual-path` (never in-place on a reviewed PR), then propagate up the stack by merge. The "test" for this work is the compiler + full CI gate: RED = build errors at `Body` sinks, GREEN = full gate passes. No new abstractions; the A/B/C convergence (store registry, typed `AppConfig`, entry-point) is **out of scope** — separate follow-on plans. +**Architecture:** trusted-server keeps its bespoke `platform/` layer (`RuntimeServices` + `PlatformConfigStore`/`SecretStore`/`KvStore`). #269's only forced code break is `Body::into_bytes() → Option` (18 sinks — Appendix A). Christian's branch already fixes those and wires `get_settings_from_services()` to rebuild `Settings` from the `app_config` config store via the shared `config_payload` flatten/hash contract. Our work is the **runtime-side hardening + spec**, not a parallel repin. -**Tech Stack:** Rust 2024, cargo, `wasm32-wasip1` (Fastly via Viceroy), edgezero git dep, `error-stack`. +**Tech Stack:** Rust 2024, cargo, `wasm32-wasip1` (Fastly via Viceroy), edgezero git dep (`2eeccc9`, #269 HEAD), `error-stack`. -**Source spec:** [2026-06-16-edgezero-269-repin-breaking-api-finding.md](../specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md) — §2 (sink list), §5 (steps), §8 (gate), §11 (stack/merge-up). +**Source spec:** [2026-06-16-edgezero-269-repin-breaking-api-finding.md](../specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md) — esp. **§12** (convergence), §2 (sinks), §9 (decisions). --- -## Scope & non-goals - -**In scope:** repin the 4 `edgezero-*` deps; fix the 18 `Body::into_bytes` sinks (8 production + 10 test); reconcile the integration-tests lockfile; drop the two never-compiled adapter deps; pass the full gate; merge up PR14→PR20. - -**Out of scope (separate plans):** store convergence onto edgezero `ConfigStore`/`SecretStore`/`StoreRegistry` (spec §6 B); typed `AppConfig` two-tier config (§6 C / Christian's CLI port); entry-point `run_app`/`app!` adoption (§6 C). Do **not** start these here. - -**Key constraints:** +## Strategy change (why this plan was rewritten) -- `as_bytes` changed but has **no** trusted-server sink — only `into_bytes` needs edits (spec §2). -- All sinks are buffered (`Once`) bodies → fix with `.expect("should …")`, never `unwrap_or_default()`. -- Line numbers below are **PR14-base**; they shift per branch. **Re-derive the exact sink set from the compiler on each branch** — do not trust hardcoded lines after PR14. -- Pin to the #269 HEAD sha while #269 is open; **re-pin to edgezero `main` after #269 merges**. +The prior version of this plan was a standalone minimal-repin off PR14. Investigation of `feature/ts-cli-next` (2026-06-18, spec §12) showed that branch is **not just CLI** — it already implements the end-to-end Fastly config-store migration: same #269 pin, the `Body` fixes, store ids, the `config_payload` contract, **and** runtime `Settings`-from-store load. So a separate Fastly repin is **redundant**. This plan now **builds on his branch** and focuses on the runtime gaps. The verified `Body`-sink enumeration is preserved as Appendix A (still the authoritative sink reference when his ad-hoc fixes merge up the stack). --- -## File structure +## Open decisions — resolve at Phase 0 before coding -| File | Change | Responsibility | -| ------------------------------------------------------------- | ---------------------------------------------------- | --------------------------- | -| `Cargo.toml` | Modify lines 59–62 | The 4 `edgezero-*` git pins | -| `Cargo.lock` | Regenerated | Root lock | -| `crates/integration-tests/Cargo.lock` | Reconcile | Shared-dep lock (CI gate) | -| `crates/trusted-server-core/src/proxy.rs` | 5 sinks (38, 1550, 1665 prod; 2034, 2795, 2851 test) | proxy/asset body reads | -| `crates/trusted-server-core/src/publisher.rs` | 4 sinks (46 prod; 748, 1079, 1562 test) | publisher body reads | -| `crates/trusted-server-core/src/auction/endpoints.rs` | 1 sink (81 prod) | auction body read | -| `crates/trusted-server-core/src/auction/formats.rs` | 1 sink (444 test) | auction test helper | -| `crates/trusted-server-core/src/request_signing/endpoints.rs` | 4 sinks (103, 246, 365 prod; 464 test) | signing endpoint body reads | -| `crates/trusted-server-core/src/integrations/prebid.rs` | 1 sink (2067 test) | prebid test | -| `crates/trusted-server-core/src/integrations/testlight.rs` | 1 sink (461 test) | testlight test | +| # | Decision | Recommendation | +| --- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| D1 | Build on `feature/ts-cli-next` vs keep the PR14-stack minimal-repin | **Build on his** (his is end-to-end for Fastly; ours duplicates it) | +| D2 | Whole-`Settings` → store (his) vs two-tier small `AppConfig` (our spec §6) | **Adopt his whole-`Settings`** (one source of truth; already implemented) | +| D3 | `Body` fix style | **His `ok_or_else` (graceful)** over `.expect()` (spec §2) | +| D4 | Empty/unseeded store behavior | **Decide explicitly** (Phase 2) — today it's a hard fail / outage | +| D5 | CLI-driven secret push (he punts) vs runtime secret writes (already exist via `management_api.rs`) | Keep runtime rotation; treat CLI secret-push as a later follow-up | +| D6 | Branch/merge topology — his branch is off `main`, the stack is PR14→PR20 | Phase 5 — confirm with team | -`http_util.rs:456` (`enforce_max_body_size(bytes: &[u8], …)`) is **not** a sink — no edit. +Do **not** start Phase 1 until **D1–D3** are confirmed (they set the base branch +and code style). **D4–D6 are sequenced, not skipped:** D4 (empty-store response) +is resolved in Phase 2 Step 5, D5 (secret-write boundary) in Phase 3 Step 2, D6 +(branch topology) in Phase 5 Step 1. --- -## Fix shapes (apply the matching one at each sink) +## Scope & non-goals -```rust -// Shape A — value consumed directly (body_as_reader; let body = …into_bytes()) -let body = resp.into_body().into_bytes() - .expect("should have a buffered body"); - -// Shape B — chained .to_vec() (String::from_utf8(…into_bytes().to_vec())) -String::from_utf8( - resp.into_body().into_bytes() - .expect("should have a buffered body") - .to_vec(), -) - -// Shape C — bound, then borrowed into &[u8]/&Bytes (enforce_max_body_size(&b)/from_slice(&b)) -let b = req.into_body().into_bytes() - .expect("should have a buffered request body"); -enforce_max_body_size(&b, …)?; -serde_json::from_slice(&b)?; -``` +**In scope:** converge onto his branch; verify the repin + `Body` fixes are complete against Appendix A; run the full gate (host, **wasm32-wasip1**, **`--all-targets`**, clippy, test) + integration-tests lockfile; harden runtime config-store loading (empty/malformed-store, seed-before-serve); confirm secrets/`ec_identity_store` KV runtime wiring; write the runtime-config-store spec; merge up the stack. -`cargo fmt` will rewrap; write the one-liner and let it format. +**Out of scope (separate plans):** the CLI crate itself (`ts config`/`audit` — Christian); CLI-driven secret push; full edgezero `run_app`/`app!`/extractor adoption (he kept the bespoke layer, so do we); non-Fastly adapter _feature_ parity beyond making them build. --- -## Task 0: Create the dedicated branch off PR14 - -**Files:** none (git only) - -- [ ] **Step 1: Branch off PR14 (not in-place, not main)** - -```bash -git fetch origin -git checkout -b feature/edgezero-269-repin feature/edgezero-pr14-entry-point-dual-path -``` - -- [ ] **Step 2: Confirm base + capture the authoritative "from" pin** - -Run: `git log -1 --format='%s' && grep -m1 'edgezero-core' Cargo.toml` -Expected: PR14 tip; the dep line prints the **base pin = `rev = "38198f9…"`**. +## File structure (what we touch / extend, on his branch) -> Pin clarity: the Goal's `170b74b` is the _stack's original_ pin (PR1–13), **not -> this branch's base.** PR14's base is `38198f9` (spec §11). The **only** -> authoritative "from" value is whatever this grep prints — use that, not a -> hardcoded sha, if the branch has advanced. +| File | Role | Our action | +| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------- | +| `Cargo.toml` / `Cargo.lock` | edgezero pinned `2eeccc9` | verify; re-pin to `main` post-merge (Phase 5) | +| `crates/trusted-server-core/src/config_payload.rs` | flatten/hash contract (shared seam) | **read-only reference** — do not fork | +| `crates/trusted-server-core/src/settings_data.rs` | `get_settings_from_services` runtime load | **harden** empty/malformed behavior (Phase 2) | +| `crates/trusted-server-adapter-fastly/src/main.rs` | entry point: build services → load settings | **harden** the settings-error path (Phase 2) | +| `edgezero.toml` | store ids: `app_config` / `secrets` / `ec_identity_store` | verify; reference in the spec | +| `crates/trusted-server-core/src/{proxy,publisher,auction/endpoints,auction/formats,request_signing/endpoints}.rs`, `integrations/{prebid,testlight}.rs` | `Body` sinks | **verify** all 18 covered (Appendix A) | +| `crates/trusted-server-adapter-{cloudflare,spin}` | stubs, untouched by him | **make build** under #269 (Phase 3) | +| `docs/superpowers/specs/-runtime-config-store.md` | the missing spec | **create** (Phase 4) | --- -## Task 1: Repin edgezero to #269 + regenerate root lock - -**Files:** Modify `Cargo.toml:59-62`, regenerate `Cargo.lock` - -- [ ] **Step 1: Repin all 4 deps** - -Replace the base pin captured in Task 0 Step 2 (`rev = "38198f9…"` on the 4 -`edgezero-*` lines) with `rev = "2eeccc9748daba92b9adf6afe4df105e79269ae9"` -(#269 HEAD). (After #269 merges, use the edgezero `main` sha instead — see spec §9.) - -- [ ] **Step 2: Resolve the lock FIRST — separate resolution failure from compile-RED** - -Run: `cargo generate-lockfile` -Expected: lock resolves with no error. **If this fails** (MSRV / feature -unification / `spin-sdk` graph — the spec's #1 transitive risk, §5.1/§10), STOP -and surface it: that is a _resolution_ break, not the expected `Body` compile-RED, -and must be triaged before continuing. +## Phase 0: Convergence decision + adopt the base -- [ ] **Step 3: Capture the RED baseline (build only after the lock resolves)** +- [ ] **Step 1: Confirm D1–D3** with the team (record in the spec §9). If D1 = "build on his," proceed; if "keep PR14-stack," fall back to Appendix B (the minimal-repin tasks). -Run: `cargo build --workspace --all-targets 2>/tmp/ez_red.log; grep -cE '^error' /tmp/ez_red.log` -Expected: ~27 errors (`E0308`/`E0599`/`E0624`). This is the RED state. (Log goes to -`/tmp` — keep build artifacts out of the repo tree.) - -- [ ] **Step 4: Sanity — every error is at a known `Body` sink file, nothing else** - -Filter by **location**, not error-kind (an unexpected break could share a kind): - -Run: +- [ ] **Step 2: Create the HTTP-layer branch off his branch** ```bash -grep -A1 '^error' /tmp/ez_red.log | grep -- '-->' \ - | grep -vE 'trusted-server-core/src/(proxy|publisher|auction/(endpoints|formats)|request_signing/endpoints|integrations/(prebid|testlight)|http_util)\.rs' \ - && echo "UNEXPECTED — stop & investigate" || echo OK -``` - -Expected: `OK` (every error points into a known sink file from §2). Any other -location ⇒ unexpected transitive break; STOP and surface it (spec §5). - -- [ ] **Step 5: Commit the repin (still RED — that's expected)** - -```bash -git add Cargo.toml Cargo.lock -git commit -m "Repin edgezero to the extensible-cli branch (stackpop/edgezero PR 269)" +git fetch origin +# Record the exact SHA — his branch is an unmerged WIP and may force-push/rebase. +git rev-parse origin/feature/ts-cli-next # note this; if he rebases, re-base from the new SHA + coordinate +git checkout -b feature/edgezero-269-http origin/feature/ts-cli-next ``` ---- - -## Task 2: Fix production sinks — `proxy.rs` - -**Files:** Modify `crates/trusted-server-core/src/proxy.rs` (sinks 38, 1550, 1665) - -- [ ] **Step 1: Confirm current errors (RED)** - -Run: `cargo check --workspace 2>&1 | grep 'proxy.rs'` -Expected: errors **within `proxy.rs`** (exact line numbers vary per branch — re-derive -from this output; do not trust the §2 PR14 numbers after PR14). Expect a `body_as_reader` -`Cursor::new(body.into_bytes())` site plus two POST-body `let body_bytes = req.into_body().into_bytes();` sites. - -- [ ] **Step 2: Fix `body_as_reader` (Shape A)** - -`Cursor::new(body.into_bytes())` → `Cursor::new(body.into_bytes().expect("should have a buffered body"))` - -- [ ] **Step 3: Fix the two POST-body bindings (Shape C)** - -`let body_bytes = req.into_body().into_bytes();` → -`let body_bytes = req.into_body().into_bytes().expect("should have a buffered request body");` - -> `replace_all` only if the two lines are byte-identical (same indentation). **Read -> each site first**; if the replace count ≠ 2, fall back to per-site edits. Both are -> production code (not test). +- [ ] **Step 3: Baseline build (inherit his state)** -- [ ] **Step 4: Verify proxy.rs errors cleared** - -Run: `cargo check --workspace 2>&1 | grep -c 'proxy.rs' ; echo done` -Expected: `0` proxy.rs errors (other files may still error — fine). +Run: `cargo build --workspace --all-targets 2>/tmp/ez_base.log; echo "exit=$?"` +Expected: **green** (his branch should already compile). If red, capture and triage before any new work. --- -## Task 3: Fix production sinks — `publisher.rs` + `auction/endpoints.rs` - -**Files:** Modify `publisher.rs` (line 46), `auction/endpoints.rs` (line 81) - -- [ ] **Step 1: Fix `publisher.rs:46` `body_as_reader` (Shape A)** - -`std::io::Cursor::new(body.into_bytes())` → `std::io::Cursor::new(body.into_bytes().expect("should have a buffered body"))` - -- [ ] **Step 2: Fix `auction/endpoints.rs:81` (Shape C)** - -`let body_bytes = body.into_bytes();` → `let body_bytes = body.into_bytes().expect("should have a buffered request body");` +## Phase 1: Verify the inherited repin + `Body` fixes -- [ ] **Step 3: Verify both cleared** +His `Body` fixes were ad-hoc (driven by his build), not enumerated. Verify completeness against Appendix A, and run the **full** gate (he is unlikely to have run wasm + `--all-targets` + clippy on every leg). -Run: `cargo check --workspace 2>&1 | grep -cE 'publisher.rs|auction/endpoints.rs'` -Expected: `0`. +- [ ] **Step 1: Enumerate the sinks (locate, don't "prove")** ---- - -## Task 4: Fix production sinks — `request_signing/endpoints.rs` - -**Files:** Modify `request_signing/endpoints.rs` (lines 103, 246, 365) +Run: `git grep -nE 'into_bytes\(\)' crates/trusted-server-core/src -- 'proxy.rs' 'publisher.rs' 'auction/endpoints.rs' 'auction/formats.rs' 'request_signing/endpoints.rs' 'integrations/prebid.rs' 'integrations/testlight.rs'` +Expect **18 sites** (8 prod + 10 test). Eyeball each has an `Option` handler +(`.ok_or_else`/`.expect`/`.unwrap_or_default`). **Note: grep cannot prove +correctness** — a fixed Shape-C `let b = …into_bytes().ok_or_else(…)?;` and a +broken bare `.into_bytes()` both contain `.into_bytes()`. This step is enumeration +only; the **authoritative completeness proof is Step 2's green `--all-targets` + +`cargo test`.** Appendix A line numbers are **PR14-base and do NOT apply** to this +`main`-based branch — trust the grep _count_ (18), not the numbers. -- [ ] **Step 1: Fix all three `req.into_body()` bindings (Shape C)** - -`replace_all` of ` let body = req.into_body().into_bytes();` → - -```rust - let body = req - .into_body() - .into_bytes() - .expect("should have a buffered request body"); -``` - -> Expect 3 identical occurrences, all production. **Read the sites first**; if the -> replace count ≠ 3 (indentation differs), fall back to per-site edits. The -> `json_response(body: String)` site (`String::into_bytes`) is a false positive — -> **do not touch** (verify by checking the receiver is `String`, not `Body`). - -- [ ] **Step 2: Verify lib/bin build is GREEN** - -Run: `cargo build --workspace` -Expected: **success** (all 8 production sinks fixed). Tests still red — next. - -- [ ] **Step 3: Commit production fixes** +- [ ] **Step 2: Full gate (the legs he likely skipped)** ```bash -git add crates/trusted-server-core/src -git commit -m "Adapt Body::into_bytes Option return at production sinks" +cargo build --workspace --all-targets +cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1 +cargo test --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo fmt --all -- --check ``` ---- - -## Task 5: Fix test sinks - -**Files:** Modify `proxy.rs` (2034, 2795, 2851), `publisher.rs` (748, 1079, 1562), `auction/formats.rs` (444), `request_signing/endpoints.rs` (464), `integrations/prebid.rs` (2067), `integrations/testlight.rs` (461) - -- [ ] **Step 1: Confirm test errors (RED)** - -Run: `cargo build --workspace --all-targets 2>&1 | grep -E '^error' | wc -l` -Expected: ~12 remaining errors, all in test code at the lines above. - -- [ ] **Step 2: Apply the matching shape at each test sink** - -- `String::from_utf8(… .into_bytes().to_vec())` → Shape B (`.expect("should have a buffered body")` before `.to_vec()`): `proxy.rs:2034`, `publisher.rs:748`, `prebid.rs:2067`, `request_signing/endpoints.rs:464`. -- `serde_json::from_slice(&… .into_bytes())` → Shape C (bind, `.expect`, borrow): `auction/formats.rs:444`, `testlight.rs:461`. -- `let x = … .into_bytes();` → Shape A (`.expect`): `proxy.rs:2795`, `proxy.rs:2851`, `publisher.rs:1079`, `publisher.rs:1562`. - -(The `request_signing/endpoints.rs:452` test helper is `str::as_bytes` — false positive, **do not touch**.) +Expected: all green. Any failure here is the real signal — fix before Phase 2. -- [ ] **Step 3: Verify `--all-targets` is GREEN** +- [ ] **Step 3: integration-tests lockfile** -Run: `cargo build --workspace --all-targets` -Expected: **success** — all 18 sinks fixed. +`crates/integration-tests` is a separate workspace that path-deps `trusted-server-core`. +Run: `( cd crates/integration-tests && cargo build --workspace )` first (don't +`generate-lockfile` — that can re-resolve and _cause_ drift). Only if it fails on +shared-dep mismatch: `cargo update -p --precise ` (never +blanket). Repeat for `crates/openrtb-codegen` if it drifts. -- [ ] **Step 4: Commit test fixes** +- [ ] **Step 4: Commit any gate fixups** ```bash -git add crates/trusted-server-core/src -git commit -m "Adapt Body::into_bytes Option return in tests" +git add crates Cargo.toml Cargo.lock && git commit -m "Complete Body sink coverage and pass full gate on #269" || echo "nothing to commit" ``` --- -## Task 6: Drop never-compiled adapter deps +## Phase 2: Runtime config-store hardening (the core HTTP-layer deliverable) -**Files:** Modify `Cargo.toml` (remove `edgezero-adapter-axum`, `edgezero-adapter-cloudflare` from `[workspace.dependencies]`) +**Problem (verified against his `main.rs`):** `get_settings_from_services` → +`get_settings_from_config_store` reads `ts-config-keys` first; on an +**empty/unseeded store** `read_config_entry`'s `?` propagates a `Configuration` +error. His settings-error arm **does serve a response** — +`to_error_response(&e).send_to_client(); return;` (not a bare return, not an +opaque default; `fn main()` returns `()` and serves explicitly). So the issue is +**not** "no response" — it is that **every route returns a generic error** until +the store is seeded, and the error is **indistinguishable from a real config +bug**. Fresh deploy before `ts config push` = **total outage with an opaque 500**. +The gap our layer owns: make the unseeded case **actionable** (clear message) and +**correctly classified** (retryable 503, not 500). -- [ ] **Step 1: Confirm they are absent from the graph** +> **Call chain (read first):** `get_settings_from_services(&runtime_services)` → +> `get_settings_from_config_store(&dyn PlatformConfigStore, &StoreName)` → +> `read_config_entry` (per key) → `settings_from_config_entries` (hash verify). +> The in-memory `PlatformConfigStore` fake **already exists** as +> `MemoryConfigStore` in `settings_data.rs` tests (around line 84) — reuse it; do +> not write a new one. Confirm the exact constructor (`MemoryConfigStore { entries }` +> vs `::new(...)`) before writing the test below. -Run: `cargo tree -i edgezero-adapter-axum; cargo tree -i edgezero-adapter-cloudflare` -Expected: both → "did not match any packages" (no member uses them — spec §1). +- [ ] **Step 1: Write a failing test — empty store yields an actionable, typed error (not a generic read failure)** -- [ ] **Step 2: Remove the two lines from `Cargo.toml` `[workspace.dependencies]`** +In `settings_data.rs` tests (reuse `MemoryConfigStore`): -Delete the `edgezero-adapter-axum = …` and `edgezero-adapter-cloudflare = …` lines (keep `edgezero-adapter-fastly` and `edgezero-core`). - -- [ ] **Step 3: Verify still builds** - -Run: `cargo build --workspace --all-targets` -Expected: success (nothing referenced them). - -- [ ] **Step 4: Commit** - -```bash -git add Cargo.toml Cargo.lock -git commit -m "Drop unused edgezero axum/cloudflare workspace deps" +```rust +#[test] +fn empty_config_store_reports_unseeded_not_generic_failure() { + let store = MemoryConfigStore::new(BTreeMap::new()); // no ts-config-keys + let err = get_settings_from_config_store(&store, &StoreName::from("app_config")) + .expect_err("empty store should error"); + // assert it carries an actionable "config store not seeded — run `ts config push`" context + assert!(format!("{err:?}").contains("not seeded") || format!("{err:?}").contains("ts config push")); +} ``` -> If a future `trusted-server-adapter-{axum,cloudflare}` consumer lands, re-add then. Skip this task if the team prefers to keep them pinned for symmetry — note the decision. +- [ ] **Step 2: Run it — verify it fails** (`cargo test -p trusted-server-core empty_config_store -- --nocapture`). Expected: FAIL (current error message is generic "failed to read … key `ts-config-keys`"). ---- - -## Task 7: Reconcile the integration-tests lockfile - -> **Ordering matters.** This runs _after_ all root dependency changes (repin Task 1 -> -> - drop-adapters Task 6) and _after_ `trusted-server-core` is GREEN (Tasks 2–5). -> `crates/integration-tests` is a **separate workspace** that path-deps -> `trusted-server-core` (`Cargo.toml:13`); building it any earlier fails on the -> Body errors, not lock drift — confounding the check. +- [ ] **Step 3: Implement — distinguish "unseeded" from "read error"** -**Files:** `crates/integration-tests/Cargo.lock` (and `crates/openrtb-codegen/Cargo.lock` if it drifts) +In `read_config_entry` / `get_settings_from_config_store`, when the **metadata** key (`ts-config-keys`) is absent, attach an actionable context (e.g. `TrustedServerError::Configuration` with `"config store `{store}`is not seeded — run`ts config push --adapter fastly`"`). Keep transport/read failures distinct. -- [ ] **Step 1: Resolve + build the integration-tests workspace** +- [ ] **Step 4: Run the test — verify it passes.** -Run: `( cd crates/integration-tests && cargo generate-lockfile && cargo build --workspace 2>&1 | tail -20 )` -Expected: lock resolves and it builds. Because core is now green, any failure here -is a _real_ signal — shared-dep drift or a genuine break — not the Body RED. +- [ ] **Step 5: Decide + implement the adapter response (D4)** -- [ ] **Step 2: If shared-dep drift, reconcile with targeted updates only** +His settings-error arm already serves via `to_error_response(&e).send_to_client(); return;`. +Two options — **decide D4 here:** +(a) keep the arm, but have `to_error_response` map the new "unseeded" error context +to **503** (retryable) instead of 500; or +(b) special-case the unseeded error in `main.rs` before `to_error_response`: +`FastlyResponse::from_status(503).with_body_text_plain("config not provisioned — run `ts config push`").send_to_client(); return;` +(matches the existing `from_status(...).with_body_text_plain(...).send_to_client()` +idiom at `main.rs:119–121`). Add an adapter test asserting **503 + body** for the +unseeded case. This turns an opaque 500 into an observable, actionable signal — +and keeps real config bugs as 500. -For each mismatched shared dep (e.g. `bytes`, `http`, `serde`): -Run: `( cd crates/integration-tests && cargo update -p --precise )` -**Never** a blanket `cargo update`. (Project CI gate: shared direct deps must match -root.) Repeat in `crates/openrtb-codegen` if it drifted. +- [ ] **Step 6: Malformed-store test** — seed a `ts-config-hash` that doesn't match the entries; assert `settings_from_config_entries` errors on hash mismatch (his code already verifies; add the test if absent so the contract is locked). -- [ ] **Step 3: Verify** +- [ ] **Step 7: Confirm secrets + KV runtime wiring** + - `secrets` store: request-signing reads signing keys via `PlatformSecretStore` (pre-existing `management_api.rs` provides write CRUD). Add/confirm a test that a missing signing secret degrades to a clear error, not a panic. + - `ec_identity_store` KV: `main.rs` starts `UnavailableKvStore` and EC routes lazily bind the configured store. Confirm a non-EC route still serves when EC KV is unavailable (existing behavior — add a regression test if missing). -Run: `( cd crates/integration-tests && cargo build --workspace )` -Expected: success. - -- [ ] **Step 4: Commit if changed** +- [ ] **Step 8: Commit** ```bash -git add crates/integration-tests/Cargo.lock crates/openrtb-codegen/Cargo.lock -git commit -m "Reconcile integration-tests lockfile after edgezero repin" || echo "nothing to commit" +git add crates && git commit -m "Harden runtime config-store load: actionable unseeded error and 503 response" ``` --- -## Task 8: Full verification gate - -**Files:** none (verification only) - -- [ ] **Step 1: Compile gate (host + all-targets)** +## Phase 3: Adapter + build-surface gaps -Run: `cargo build --workspace --all-targets` -Expected: success. +- [ ] **Step 1: Make non-Fastly adapters build under #269** -- [ ] **Step 2: wasm32-wasip1 (Fastly deploy target)** +First confirm what "builds" means for these stubs — spec §1 notes +cloudflare/axum are **absent from the dependency graph** (not currently compiled). +If the crate has no real wasm entry, "builds" = `cargo check -p trusted-server-adapter-cloudflare` +on host; only use `--target wasm32-unknown-unknown` (install the target first) if +it has a genuine worker entry point. Same judgment for spin. +If they break on `Body`/edgezero churn, apply the Appendix A fix shapes. They are stubs — goal is **compiles**, not feature parity (out of scope). -Run: `cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1` -Expected: success. (First leg not yet verified at spec-freeze — this is the gate that proves it.) +- [ ] **Step 2: Document the secret-write boundary (D5)** -- [ ] **Step 3: Tests** +Confirm: runtime key-rotation secret writes work via `management_api.rs` (pre-existing); CLI-driven secret _push_ is deferred (Christian punts it). Capture this split in the spec so it is a recorded decision, not an accident. -Run: `cargo test --workspace` -Expected: pass. Watch for behavioral diffs in body-handling tests (they exercise the `.expect()` paths). - -- [ ] **Step 4: Clippy + fmt** - -Run: `cargo clippy --workspace --all-targets --all-features -- -D warnings && cargo fmt --all -- --check` -Expected: clean. - -- [ ] **Step 5: integration-tests + JS gates (per CLAUDE.md CI)** - -Lock already reconciled (Task 7) — this just runs the suites. -Run: `( cd crates/integration-tests && cargo test --workspace )` and `( cd crates/js/lib && npx vitest run )` -Expected: pass (JS untouched — sanity only). - -- [ ] **Step 6: Commit any fmt fixups** - -```bash -git add crates Cargo.toml Cargo.lock && git commit -m "Verification gate fixups" || echo "nothing to commit" -``` - -(Scope the add — never `git add -A`, which would sweep stray build logs into the commit.) +- [ ] **Step 3: Commit any adapter fixups.** --- -## Task 9: Open the PR (PR14-based dedicated branch) - -**Files:** none +## Phase 4: Runtime-config-store spec (the doc his CLI design references but never wrote) -- [ ] **Step 1: Push + open PR targeting the PR14 branch (or wherever the stack lands)** - -```bash -git push -u origin feature/edgezero-269-repin -gh pr create --base feature/edgezero-pr14-entry-point-dual-path \ - --title "Repin edgezero to #269 and adapt Body::into_bytes Option return" \ - --body "See docs/superpowers/specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md" -``` +- [ ] **Step 1: Write `docs/superpowers/specs/-runtime-config-store.md`** covering: + - the load sequence (`build_runtime_services` → `get_settings_from_services` → `settings_from_config_entries`); + - the **shared `config_payload` contract** (escaping, sorted-key canonicalization, `sha256` over settings-only entries, `ts-config-*` reserved keys) — reference, do not duplicate; + - the **seed-before-serve** operational contract + the 503 unseeded behavior (Phase 2); + - empty / missing-key / malformed-hash / transport-error matrix; + - store-name resolution + `EDGEZERO__STORES__CONFIG__APP_CONFIG__NAME` override; + - secrets/KV runtime read paths + the secret-write boundary (D5); + - non-Fastly adapter status. -> Do not push or open the PR until the user approves (per project git rules). Confirm the base branch with the team — the stack tip may have advanced. +- [ ] **Step 2: Docs gate** — `cd docs && npm run format` (prettier-clean), then commit. --- -## Task 10: Propagate up the stack (merge, not rebase) +## Phase 5: Stack propagation + re-pin -**Files:** none (per-branch merge + gate) +- [ ] **Step 1: Reconcile topology (D6).** His branch is off `main`; the migration stack is PR14→PR20. Confirm with the team whether the HTTP-layer branch merges via `main` (with his) or threads the stack. Do not push/merge without approval. -> **Approval gate:** merging up mutates 6 review branches. Do **not** run this task -> (or any `git push`) until the user approves — same rule as Task 9. +- [ ] **Step 2: Re-pin to edgezero `main` after #269 merges** — one-line dep change in `Cargo.toml`, regenerate lock, re-run the Phase 1 gate. -For each branch PR15 → PR16 → PR17 → PR18 → PR19 → PR20, in order: +- [ ] **Step 3: Open the PR (approval-gated).** Base = whatever Step 1 resolves. Assign `@me`. Summary: HTTP-layer convergence + runtime hardening + spec. -- [ ] **Step 1: Merge the repin forward** - -```bash -git checkout feature/edgezero-pr15-remove-fastly-core -git merge feature/edgezero-269-repin # merge, not rebase (team preference) -``` - -- [ ] **Step 2: Re-derive sinks from the compiler (line numbers/sink set shift per layer)** - -Run: `cargo build --workspace --all-targets 2>&1 | grep -E 'into_bytes|Body'` -Resolve any new/moved sinks with the §2 fix shapes. PR15 (remove-fastly-core) and PR16+ move/delete these files — expect manual conflict resolution, not clean fast-forward. - -- [ ] **Step 3: Run the full gate (Task 8) on this branch** - -Expected: green before moving to the next branch up. - -- [ ] **Step 4: Commit the merge resolution (if any)** +--- -```bash -git add -A && git commit --no-edit || echo "nothing to commit (clean fast-forward)" -``` +## Risks & watch points -Repeat for each branch up to PR20. +| Risk | Mitigation | +| ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Fresh deploy = outage** (unseeded store, no fallback) | Phase 2: actionable error + 503; document seed-before-serve; consider a provisioning gate in deploy | +| His `Body` fixes incomplete vs our 18 sinks | Phase 1 Step 1 cross-check against Appendix A | +| He likely didn't run wasm + `--all-targets` + clippy on every leg | Phase 1 Step 2 runs the full matrix | +| Pinned to an **open, force-pushable** #269 ref | Re-pin to `main` post-merge (Phase 5); rollback = revert the dep commit | +| **Building on a colleague's unmerged WIP branch** (`feature/ts-cli-next`) — it may rebase/force-push out from under us, vanishing our merge-base | Record its SHA at Phase 0 Step 2; if he rebases, re-base from the new SHA and coordinate before any merge; keep our additions as discrete commits so they re-cherry-pick cleanly | +| integration-tests lockfile drift | Phase 1 Step 3, targeted `--precise` only | +| Branch topology (his off `main`, stack off PR14) | Phase 5 Step 1, confirm with team | +| Whole-`Settings`-in-store enlarges blast radius of a bad push | hash verification (his) + malformed-store test (Phase 2 Step 6) | --- -## Follow-on plans (NOT this plan) +## Appendix A — verified `Body::into_bytes` sink reference (authoritative) + +From the compiler spike (spec §2/§10): **18 sink bindings, 8 production + 10 test-only**, all `into_bytes` (no `as_bytes` sink). The line numbers below are **PR14-base — they do NOT apply to the `main`-based `feature/ts-cli-next`**; use them only as a count/shape reference (8 prod + 10 test). On any branch, the compiler (`--all-targets`) is the source of truth. Use this to confirm Christian's ad-hoc fixes are complete and when merging up the stack. -Per spec §6/§9, after the repin lands and #269 merges to edgezero `main`: +- **Production (8):** `proxy.rs:38`, `publisher.rs:46`, `auction/endpoints.rs:81`, `proxy.rs:1550`, `proxy.rs:1665`, `request_signing/endpoints.rs:103/246/365`. +- **Test-only (10):** `auction/formats.rs:444`, `prebid.rs:2067`, `testlight.rs:461`, `proxy.rs:2034/2795/2851`, `publisher.rs:748/1079/1562`, `request_signing/endpoints.rs:464`. +- **Not a sink:** `http_util.rs:456` (the `enforce_max_body_size(bytes: &[u8], …)` signature). +- **Fix style (D3):** production → `into_bytes().ok_or_else(|| )?`; compression/test → `unwrap_or_default()`; only `.expect("should …")` where a buffered body is truly invariant. -1. **Re-pin to `main`** (one-line dep change + gate). -2. **Store convergence** (spec §6 B): map `PlatformConfigStore`/`SecretStore` reads onto edgezero `ConfigStore`/`SecretStore`/`StoreRegistry` + thin write-CRUD extension. -3. **Typed `AppConfig` (two-tier)** + **CLI port** (spec §6 C / §7) — Christian. Shared contract = the config struct + `[stores.config]` id; agree before either starts. +## Appendix B — fallback: standalone minimal-repin (only if D1 = "keep PR14 stack") -Each gets its own spec → plan cycle. +If the team rejects building on his branch, the original minimal-repin still applies: branch off PR14, repin to `2eeccc9`, fix the Appendix A sinks with the D3 style, reconcile the integration-tests lock, full gate, merge up PR14→PR20. (This duplicates his Fastly work and is **not** recommended — see spec §12.) diff --git a/docs/superpowers/specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md b/docs/superpowers/specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md index 35b6a089..c9c55412 100644 --- a/docs/superpowers/specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md +++ b/docs/superpowers/specs/2026-06-16-edgezero-269-repin-breaking-api-finding.md @@ -65,6 +65,15 @@ convergence + typed config + entry-point) is a _separate, optional_ track — **not** forced by the repin (§11). +8. **Superseded for Fastly by `feature/ts-cli-next` (§12).** Christian's "CLI" + branch already implements the end-to-end Fastly config-store migration — same + #269 pin, the `Body` fixes (graceful `ok_or_else`, not `.expect()`), the store + ids, the `config_payload` flatten/hash contract, **and runtime + Settings-from-store load** (`get_settings_from_services`). So our minimal-repin + (#771) is largely redundant for Fastly. **Revised: build on his branch**; our + real HTTP-layer deliverable is the **runtime-config-store spec** his CLI doc + references but never wrote. See §12. + --- ## 1. What trusted-server actually consumes from edgezero @@ -86,6 +95,10 @@ of `edgezero-core` plus one type from `edgezero-adapter-fastly`. `ProxyClient`/edgezero `proxy`, typed `AppConfig`, manifest `[stores.*]` / `[adapters.*]` tables. trusted-server's manifest is `trusted-server.toml` (a bespoke `Settings` struct in `settings.rs`), **not** an edgezero `edgezero.toml`. +(Baseline as of the current pin / pre-`ts-cli-next`. Christian's branch adds an +`edgezero.toml` and deletes `trusted-server.toml` — but still reads config through +the **bespoke `PlatformConfigStore`**, not edgezero's first-class store/extractor, +so this "uses none of …" list stays true even there. See §12.) --- @@ -157,11 +170,30 @@ not `Body`; confirmed by source): `sourcepoint.rs:571` / `datadome.rs:323` (`rewrite_script_content() -> String`), `sourcepoint.rs:822` (`FromUtf8Error::into_bytes`). -### Fix — three distinct shapes +### Fix — style (updated per `feature/ts-cli-next`) + +> **Revised guidance.** Christian's branch already fixed these sinks and chose +> **`into_bytes().ok_or_else(|| )?`** (graceful error, no panic) for +> production request/response handlers, `unwrap_or_default()` for +> compression/test paths, and reserved `.expect()` for genuinely-unreachable +> spots. That is **better than a blanket `.expect()`** — a streaming/empty body +> must not panic the worker. **Adopt his approach:** propagate an error at +> production handler sinks; only `.expect("should …")` where a buffered body is +> truly invariant (and never `unwrap_or_default()` where an empty body would +> silently corrupt behavior). Align with his exact per-sink choices when we +> converge (§12). -All sinks are buffered (`Once`) bodies, so use an explicit `.expect()` with a -`should`-style message (per CLAUDE.md), **not** `unwrap_or_default()`. If a future -sink is genuinely streaming, branch on `None` / use `into_stream()`. +For a production handler sink, prefer (error variant illustrative — match the +existing one at each call site, not necessarily `BadRequest`): + +```rust +let bytes = req.into_body().into_bytes().ok_or_else(|| { + Report::new(TrustedServerError::BadRequest { message: "request body should be buffered".into() }) +})?; +``` + +The three mechanical shapes below still apply (substitute `ok_or_else(…)?` for +`.expect(…)` at production sinks): ```rust // Shape A — value consumed directly (e.g. proxy.rs:38, publisher.rs:46) @@ -468,9 +500,22 @@ shift per layer. **Still open:** -1. Typed-config struct + `[stores.config]` id: the shared HTTP/CLI contract — - agree with Christian before either port lands (roadmap, not repin). -2. wasm32-wasip1 + clippy + test legs of the verification gate (§10 covered host +1. **Convergence with `feature/ts-cli-next` (§12).** Christian's branch already + implements the end-to-end Fastly config-store migration (repin + Body fix + + runtime Settings-from-store), so our minimal-repin (#771) is largely subsumed + for Fastly. Decide: rebase our HTTP work onto his config system vs keep the + PR14-stack repin. **Recommend: build on his.** +2. **Body-fix style conflict** — his `ok_or_else` (graceful) vs our former + `.expect()`. Resolved in §2 (adopt his); confirm per-sink alignment on merge. +3. **Secret-write conflict** — he punts secret-store writes (key rotation) until + edgezero exposes write primitives; our original migration design kept + write-CRUD in TS. Decide which holds. +4. **Shared config contract — now CONCRETE, not "to agree":** store ids + `app_config` / `secrets` / `ec_identity_store` (his `edgezero.toml`) and the + `config_payload` flatten/hash rules (his core module). The remaining gap is + the **runtime-config-store spec** his doc references but never wrote — that is + our HTTP-layer deliverable (§12). +5. wasm32-wasip1 + clippy + test legs of the verification gate (§10 covered host build only). --- @@ -555,3 +600,82 @@ gets the same build signal without disturbing any open review. Conflicts will cluster in the files every layer rewrites — `publisher.rs`, `proxy.rs`, `request_signing/endpoints.rs`, `auction/endpoints.rs`. Run the §10 gate (host + wasm + clippy + test) **per branch as the pin advances**, not once. + +--- + +## 12. Convergence with `feature/ts-cli-next` (Christian's branch) + +Inspected 2026-06-18. The "CLI" branch is **not just CLI** — it is an +**end-to-end config-store migration for the Fastly adapter**, off `main`, pinned +to the **same #269 HEAD** (`2eeccc9`) we used. It already carries the repin, the +`Body` fixes, the CLI crate, _and_ runtime Settings-loading from the config store. +This materially changes our plan. + +### 12.1 What it already implements (overlaps our work) + +| Area | His branch | Effect on us | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| #269 repin | deps pinned `2eeccc9` | our repin (#771) duplicated for Fastly | +| `Body` sink fixes | `into_bytes().ok_or_else(…)?` (prod), `unwrap_or_default()` (compress/test) | supersedes our `.expect()` style (§2) | +| Store ids | `edgezero.toml`: config `app_config`, secrets `secrets`, kv `ec_identity_store` | the shared seam, concrete | +| Config contract | `trusted-server-core/src/config_payload.rs` — flatten all of `Settings` → entries + `sha256` (`ts-config-hash`/`ts-config-keys`), reversible | shared core module; CLI pushes, runtime reads | +| **Runtime load** | adapter `main.rs`: `build_runtime_services()` → `get_settings_from_services()` reads `app_config`, rebuilds `Settings` | **this is the HTTP-layer pattern, already wired for Fastly** | +| `trusted-server.toml` | **deleted**; replaced by `trusted-server.example.toml` | config now lives in the store, seeded via `ts config push` | + +### 12.2 The HTTP-layer pattern, demonstrated + +`crates/trusted-server-adapter-fastly/src/main.rs` (his branch): + +```rust +let runtime_services = build_runtime_services(&req, kv_store); // config store available first +let settings = match get_settings_from_services(&runtime_services) { … }; // load Settings FROM store +``` + +`get_settings_from_services` → resolves the store name via +`env_config.store_name("config", DEFAULT_CONFIG_STORE_ID)` (`= "app_config"`) → +reads `ts-config-keys` / `ts-config-hash` / each entry → +`settings_from_config_entries` (verifies hash) → `Settings`. **That entry-point +sequence is exactly what "our HTTP layer" was meant to build.** + +**Crucial: he reads through the bespoke `PlatformConfigStore`, not edgezero's +store surface.** `services.config_store()` returns `&dyn PlatformConfigStore` +(impl `FastlyPlatformConfigStore`); he uses **none** of edgezero's #269 +`ConfigStore`/`StoreRegistry`/`Config` extractor/`RequestContext` (grep-confirmed: +zero in his `main.rs`/`settings_data.rs`). So he converged the config **source** +(toml → store) while **keeping `RuntimeServices` + the `platform/` layer** — i.e. +he took our §6 **hybrid** path, not full edgezero adoption. This also keeps §1's +"uses none of …" list accurate on his branch, and validates that our HTTP layer +should bind the store via `PlatformConfigStore`, not edgezero's extractor. + +### 12.3 Runtime contract (new, important) + +A **missing key is a hard error** — there is **no `trusted-server.toml` +fallback** anymore. The settings-error arm in `main.rs` **does serve a response** +(`to_error_response(&e).send_to_client(); return;`) — so an unseeded store yields +a **generic 500 on every route** (not a silent no-response), and that 500 is +**indistinguishable from a real config bug**. Net: the worker **cannot serve real +routes until the store is seeded** (`ts config push`). This is a new +deploy-ordering requirement (seed-before-serve) and an operational risk — the HTTP +layer should make the unseeded case **actionable** (clear message) and **correctly +classified** (retryable 503, not 500). See the plan's Phase 2. + +### 12.4 Conflicts to resolve (see §9) + +1. **Body-fix style** — adopt his `ok_or_else` (done in §2); align per-sink. +2. **Secret writes** — he punts key-rotation secret writes until edgezero adds + write primitives; our original migration design kept write-CRUD in TS. Decide. +3. **Base branch** — his off `main`; ours off the PR14 stack. His already carries + repin+Body, so for Fastly our minimal-repin is redundant. +4. **Whole-`Settings` vs two-tier** — he flattens _all_ of `Settings` into the + store (not our two-tier small `AppConfig`). One source of truth; bigger blast. + +### 12.5 Revised recommendation + +- **Build on his branch, don't run a parallel repin.** Our #771 minimal-repin is + superseded for Fastly; keep it only as the verified breaking-API reference. +- **Our HTTP-layer deliverable = the "runtime-config-store spec" his CLI doc + references but never wrote** — document `get_settings_from_services`, the + flatten/reconstruct rules (shared `config_payload`), the seed-before-serve + contract, empty/malformed-store behavior, and non-Fastly adapter wiring. +- **Keep our compiler-verified `Body` enumeration (§2/§10)** as the authoritative + sink reference when merging his ad-hoc fixes up the stack.