Skip to content

Outbound HTTP design spec - #275

Draft
aram356 wants to merge 297 commits into
mainfrom
docs/outbound-http-spec
Draft

Outbound HTTP design spec#275
aram356 wants to merge 297 commits into
mainfrom
docs/outbound-http-spec

Conversation

@aram356

@aram356 aram356 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements and specifies EdgeZero portable outbound HTTP across core and the Axum, Cloudflare, Fastly, and Spin adapters.

  • Adds independent encoded transport, decoded output, and final buffered response limits.
  • Adds typed outbound transport, protocol, decode, timeout, and limit provenance.
  • Adds index-aligned per-slot timing with explicit timing-quality capabilities.
  • Adds concurrent batch isolation and adapter capability reporting.
  • Adds stable monotonic time ownership through EdgeZero public types.
  • Adds bounded, deadline-aware, typed config and secret extraction.
  • Adds ingress admission foundations: stable route resolution and metadata, request start, absolute body-read deadline, one request-owned grant, lazy bounded body reads, and exactly-once state transitions.
  • Adds response-egress policy, observer, accounting, deadline, and exactly-once completion foundations.
  • Keeps core and shared adapters portable without a Tokio dependency.

Evidence boundaries

The capability matrix is intentionally fail-closed where the platform contract is not yet proven:

  • Axum raw request-target/header accounting and strict CL/TE rejection still require an audited Hyper parser hook or fork. Normalized-header checks are defense in depth only; both raw-ingress capabilities remain Unsupported.
  • Transport-observable response write completion, socket abort, disconnect cancellation, and backpressure remain outside current adapter ownership. All four response-egress transport capabilities remain Unsupported.
  • Provider and SDK allocations that occur before guest-visible values, plus other host-only accounting, are explicitly excluded. Complete resource-accounting capabilities remain Unsupported.
  • Cooperative deadline implementations do not claim preemption of synchronous or already-materialized host calls.

Validation

  • cargo test --workspace --all-targets
  • cargo test --doc --workspace
  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare
  • cargo check -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly
  • cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin
  • node scripts/check_outbound_docs_contract.mjs
  • npm --prefix docs run format
  • npm --prefix docs run lint

aram356 added 30 commits April 29, 2026 17:05
Investigated removing the allow: 40 sites in edgezero-core alone (every
public error type and handle: EdgeError, KvError, SecretError,
ConfigStoreError, ConfigStoreHandle, plus the entire Manifest* family).
The renames would force consumers in 4 adapter crates + cli + demo to
either write `kv::Error`/`secret::Error`/etc. at every callsite or set
up `use ... as KvError` aliases — a net loss in readability for a
deliberately-prefixed cross-crate API.

Replaced the terse comment with a longer one documenting the audit and
why the allow is load-bearing rather than a leftover.
Attempted the rename and surfaced three blockers:

  1. `proxy::Request`/`proxy::Response` would collide with
     `http::Request`/`http::Response` already imported at every
     consumer; the only non-colliding alternatives (`OutboundRequest`,
     `Outbound`) are strictly more verbose than `ProxyRequest`.
  2. `manifest.rs` has 17 `Manifest*` types used directly by adapters,
     cli, demos, scaffold templates, and the `#[app]` macro output.
     Stripping the prefix would force every site to write
     `use edgezero_core::manifest::Spec as Manifest` etc.
  3. The macro emits code that references these names by their current
     spelling; renaming requires regenerating every app and updating
     CLAUDE.md examples.

The lint's intent (the std-style `module::Type` idiom) is sound but
fights this crate's flat re-export surface, and several names cannot
be deprefixed without losing meaning. Allow stays with the audit
documented inline.
Two sites in middleware.rs computed `start.elapsed().as_secs_f64() *
1000.0` to get milliseconds with sub-ms precision for the
request-logging line. Sub-ms precision in a log line is unnecessary —
switch to `Duration::as_millis()` (returns `u128`) and drop the
`{:.2}` format spec. No precision loss that any reader would notice;
removes the only float-arithmetic site in the workspace.
Audit: only `Body { Once, Stream }` triggers the lint workspace-wide.
Marking it `#[non_exhaustive]` would force `_ => unreachable!()` at
each of the 37 external match sites in the four adapter crates, and
a third Body variant would silently `panic!` at runtime instead of
producing a compile error at every consumer. Body is intentionally
closed; the lint is genuinely incompatible with the design.
Add `#[inline]` to every public function and trait method across the
workspace. Touches 44 files: edgezero-core (~242 sites) and the four
adapter crates. Placement is right above the `pub fn` after any doc
comments and `#[must_use]`. No `#[inline(always)]` — leaving the call
to rustc/LLVM, which is the actual inlining decision-maker.

Note: the original workspace-allow rationale ("rustc/LLVM make better
choices than us") is still half true — the lint just wants the *hint*
present, even though rustc inlines monomorphised generics aggressively
without it. Adding the hint is cheap and the lint is satisfied.
Defends against the CodeQL `rust/cleartext-logging` rule, which heuristically
flagged `log_store_bindings` because it pipes
`manifest_data.secret_store_name(adapter)` into `log::info!`. The method
returns the binding identifier from `edgezero.toml` (e.g. `"MY_SECRETS"`),
not the secret value — but the function name pattern triggers the
analyzer's "credential getter" heuristic. Renaming to
`secret_store_binding` makes the intent unambiguous and the alert no
longer fires. Also reorders the impl method block so
`secret_store_binding` lands before `secret_store_enabled` per
`arbitrary_source_item_ordering`.
GitHub deprecated Node 20 as the JavaScript actions runtime on
2025-09-19; v4 of these three actions still ships Node 20 and triggers
the deprecation warning on every CI run. v5 majors ship the Node 24
binary and the warning goes away. All three v5 majors are stable;
the bump is mechanical and covers test.yml, format.yml,
deploy-docs.yml, and codeql.yml (11 sites total).
Previous commit only went to v5 for the three Node-deprecation actions.
Audit of all actions used across the four workflows shows five more
behind by one or two majors:

  actions/checkout                 v5 → v6
  actions/setup-node               v5 → v6
  actions/configure-pages          v4 → v6
  actions/deploy-pages             v4 → v5
  actions/upload-pages-artifact    v3 → v5

All other pins are already current:
  actions/cache                                       v5  (latest)
  actions-rust-lang/setup-rust-toolchain              v1  (latest)
  github/codeql-action/{init,analyze}                 v4  (latest)
CodeQL's `rust/cleartext-logging` rule (alert #7) taints any value
returned by a function whose name contains "secret" — it can't tell
configuration metadata (the binding identifier from edgezero.toml)
from secret material. The previous rename
`secret_store_name → secret_store_binding` did NOT defeat the
heuristic because "secret" is still in the function name.

Real fix: stop logging the binding name. Operators can read their
own `edgezero.toml` to verify which store binding was configured.
The presence message ("secrets enabled for axum") is still emitted,
which is the only thing the log line was actually load-bearing for.

Updated the affected unit test assertion to match the new wording.
Same heuristic as alert #7 — CodeQL taints any value returned by a
function whose name contains "secret" and tracks it through to HTTP
sinks. The test helper `start_test_server_with_secret_handle` was
flagged because its return value's `base_url` flowed into
`reqwest::Client::get(url)`.

Rename the helper to `start_test_server_with_store_handle` and the
return struct to `TestServerWithStore`. Functionally identical — the
test just bootstraps a dev server with an optional handle. The
remaining `with_secret_handle` builder method on `AxumDevServer` is
unaffected because it returns `Self`, not a sink-bound value.
Three real coverage gaps from earlier commits were untested:

  1. `KvStore::put_bytes_with_ttl` overflow error path
     (axum/PersistentKvStore). Asserts `Duration::MAX` triggers
     `SystemTime::checked_add` overflow and surfaces as
     `KvError::Internal("ttl overflows system time")`.
  2. `Manifest::try_load_from_str` Err path. Two cases: invalid TOML
     bytes and a manifest that fails `validator` (empty config-store
     name). Both should return `io::ErrorKind::InvalidData`.
  3. `GeneratorError::Format` smoke test. The variant cannot fire in
     practice (write-to-String is infallible), but it is part of the
     public error surface and the `From<fmt::Error>` wiring must keep
     working — assert construction + Display.

Existing coverage for the other behaviour-affecting changes was
already adequate: `KvStore::exists` is exercised by the
`contract_exists` macro across every impl plus 3 dedicated unit
tests, and `Hooks` default-method overrides are exercised by the
`TestHooks`/`DefaultHooks` tests already in app.rs.
ctor 1.0 requires explicit `#[ctor(unsafe)]` to acknowledge that
pre-main static-initialisation runs without the usual Rust safety
guarantees. The annotation is an attribute argument, not an
`unsafe { }` block, so the workspace `unsafe_code = "deny"` lint is
still satisfied. Updated the four adapter cli.rs files
(axum/cloudflare/fastly/spin).

spin-sdk 6.0 is NOT bumped: it raises the MSRV to rustc 1.93 but the
workspace ships rustc 1.91.1 (.tool-versions). Pin stays at 5.2 with
an explanatory comment until we bump the toolchain.
Bumps `.tool-versions`:
  rust       1.91.1   →  1.95.0
  viceroy    0.16.4   →  0.17.0

Both viceroy 0.17 and spin-sdk 6.0 raised their MSRV to rustc 1.93/1.95
respectively. We can now take viceroy 0.17 freely; spin-sdk 6.0 has
breaking API changes (Method variants → http::Method constants,
`IncomingRequest` removed, Builder::build() → .body()) and is left at
5.2 with a TODO until a focused migration PR.

New 1.95 clippy lints fixed in-place:
  - `result_map_unwrap_or_default`: `.map(p).unwrap_or(false)` → `.is_ok_and(p)` (2 sites)
  - `manual_map`: `.map(x).unwrap_or(default)` → `.map_or(default, x)` (1 site)
  - `duration_suboptimal_units`: `Duration::from_secs(60)` → `from_mins(1)` in
    non-const contexts. Two const items keep `from_secs(60 * 60 * 24 * 365)`
    with a localized `#[expect(clippy::duration_suboptimal_units, reason =
    "from_days/from_mins not stable in const context")]` because
    `Duration::from_{mins,days}` const variants are still nightly-only.
  - `to_string_in_format_args` / `inefficient_to_string`: replaced two
    `ToString::to_string` / `str::to_string` with `str::to_owned`
  - `missing_inline_in_public_items`: added `#[inline]` to two proc-macro
    entrypoints in edgezero-macros, three EnvOverride methods + the
    `env_guard` helper in axum/test_utils, and `From<Action>` for
    AdapterAction in cli/adapter.rs
  - `doc_paragraph_terminators`: added trailing punctuation to clap doc
    comments on every variant/field of `Command`/`NewArgs` (cli/args.rs)
    and the `KV_TABLE` doc in axum/key_value_store.rs

Docs:
  - CLAUDE.md "Rust": 1.91.1 → 1.95.0
  - CLAUDE.md "Fastly CLI": v13.0.0 → 15.1.0
  - Fix typo `fasltly` → `fastly` in .tool-versions; remove dup line
  - examples/app-demo/.../rust-toolchain.toml: 1.91.1 → 1.95.0
  - test.yml: drop the now-stale "1.91 MSRV constraint" comment on the
    viceroy install step
Both warnings sat behind `#[cfg]` gates that the `--all-features`
build profile hid:

1. `fastly::init_logger` (no-features stub) needed `#[inline]` —
   `missing_inline_in_public_items` only fires when the stub branch
   is selected, i.e. when the `fastly` feature is off.
2. `cli::dev_server::EchoParams` (no-`dev-example` build) was
   defined after `default_router`/`build_dev_router`; the canonical
   item ordering wants structs before fns at module level. Moved
   `EchoParams` to the top of the module so the order is correct
   in either feature profile.

Surfaces only via `cargo clippy --workspace --all-targets` (no
`--all-features`); the existing CI runs `--all-features` so we did
not catch this until now.
The `https://wasmtime.dev/install.sh` script broke as of 2026-05-19:
its version-detection interpolation failed and it tried to download
literal version `{`, causing the spin-wasm-tests CI job to fail
("Could not download Wasmtime version '{'").

Replace the install path with a direct GitHub-release tarball
download, pinned to the version recorded in `.tool-versions` (same
single-source-of-truth pattern already used for rust + viceroy).
Adds `wasmtime 44.0.1` to `.tool-versions` and a `Resolve Wasmtime
version` step in the workflow that greps it out.
1. `pub_with_shorthand` comment direction was reversed in the
   workspace `Cargo.toml`. Confirmed by removing the allow: 6 sites
   fire `usage of \`pub\` without \`in\`` (i.e. clippy flags
   `pub(crate)` and wants `pub(in crate)`). Restore the allow with
   wording that matches the actual lint direction and reflects
   the audited 6-site count.

2. Workspace `.cargo/config.toml` was hard-coding the
   `wasm32-wasip1` runner to Viceroy, which silently broke
   `cargo test -p edgezero-adapter-spin --target wasm32-wasip1`
   from the workspace root (used viceroy host ABI instead of
   wasmtime).

   Fix: remove the workspace-level runner entirely and add a
   per-package config for spin (`crates/edgezero-adapter-spin/
   .cargo/config.toml`) that selects `wasmtime run`. Fastly
   already had its own per-package config. CI continues to
   override via `CARGO_TARGET_WASM32_WASIP1_RUNNER` env var, so
   workspace-root invocations work in CI without the global
   default.

3. Add a module-level doc comment at the top of
   `crates/edgezero-adapter-spin/tests/contract.rs` explaining
   that the tests cover internal router/dispatch logic, NOT the
   Spin host ABI (no `spin_sdk`/WIT imports). A breaking change
   in the Spin runtime's WIT would not be caught here.
`parse_handler_path` previously panicked on a syntactically-invalid
handler path in `edgezero.toml`, which rustc surfaced as a confusing
"proc-macro panicked" message. Refactor to return `Result<ExprPath,
String>`; `build_middleware_tokens` and `build_route_tokens` propagate
the error; `expand_app` returns `compile_error!()` with the message,
matching the existing error path for manifest read/parse/validation
failures.

Two new tests: parse_handler_path_accepts_absolute_crate_path (happy
path) and parse_handler_path_rejects_invalid_syntax_with_message
(asserts the error message names the failure and echoes the offending
input).

Addresses the PR review comment on `crates/edgezero-macros/src/app.rs`.
PR reviewer claimed the lint warns *against* longhand and recommends
shorthand (i.e. our `pub(crate)` use should never fire it). Verified
empirically — removing the allow on clippy 1.95 produces 6 errors:

  error: usage of `pub` without `in`
    |  pub(crate) fn decompress_body(...)
    |  ^^^^^^^^^^ help: add it: `pub(in crate)`
    = help: ...index.html#pub_with_shorthand

So `pub_with_shorthand` flags `pub(crate)` and suggests `pub(in crate)`;
the reviewer's reading is 180° off. Quote the diagnostic in the comment
itself so future maintainers don't fall into the same trap.
Sub-project #1 of 7 in the CLI extensions roadmap. Turns edgezero-cli into
lib + bin, exposes per-command Args structs and run_* functions for
downstream projects to compose their own CLIs via clap subcommand
flattening, and adds app-demo-cli as the canonical consumer.

Force-added because docs/superpowers/ is gitignored project-wide for plans;
this spec is shared design intent and meant to be reviewed in the repo.
Replaces the sub-project-#1-only spec with a single design document that
covers the full effort: extensible edgezero-cli library, generator updates
for <name>-cli and <name>.toml scaffolding, per-service typed app-config
schema with validator integration, four new commands (auth, provision,
config validate, config push), shell-out mocking via a private
CommandRunner trait, and the app-demo overhaul that exercises everything
end-to-end.

Implementation still ships in 7 incremental PRs but the design decisions
live in one place so reviewers see the whole picture.

Force-added because docs/superpowers/ is gitignored project-wide.
High-severity fixes:
- Add --manifest to ProvisionArgs and ConfigPushArgs (matches validate)
- Update Wrangler invocations to 3.60+ syntax (space-form, --namespace-id)
- Persist provisioned IDs in edgezero.toml [stores.*.adapters.<x>].id;
  cross-write to per-adapter manifests where deploys need them
- Mermaid diagram in §3 replacing ASCII art

Medium-severity fixes:
- config push runs strict validation as pre-flight (no separate flag)
- Move --adapter to each AuthSub variant so UX is `auth login --adapter X`
- Constrain typed config push to serde_json::to_value(C) -> Object;
  document flatten / rename / skip / Option::None handling
- Unify raw + typed serialization rules; raw drops Validate + secret skip
- Replace CommandRunner positional args with CommandSpec struct
  (program, args, cwd, stdin, env)
- "Backwards-compatible" language replacing "unchanged" for default bin
- Move walkthrough doc to docs/guide/ with explicit sidebar update

Low + open questions:
- Document consumer-facing Cargo feature names and adapter opt-outs
- Generator migration note: sub-project 1 outputs don't auto-migrate
- Deprecate [stores.config.defaults] in favor of <name>.toml [config]
- Mark Spin provision / config push as "not yet supported" with pointer
  to the in-flight Spin stores PR; clear error message until then

Secret annotation:
- New §6.6 documenting #[derive(AppConfig)] from edgezero-macros
- #[secret] field attribute marks runtime-secret-store-backed fields
- Toml value for those fields is the secret-store binding name
- config validate (typed) cross-checks the binding appears in [stores.secrets]
- config push (typed) skips SECRET_FIELDS entirely

The implementation still ships in 7 incremental PRs.
…cope

Manifest schema rewrite (new sub-projects #2 and #3):
- [stores.<kind>].ids = [...] + default declare the logical stores the
  app uses (kv, secrets, config all multi-store)
- [adapters.<X>.stores.<kind>.<id>].name = "..." maps each logical id to
  the platform-specific name on adapter X, with optional adapter-specific
  tuning fields stored as free-form extras
- Provisioned platform resource IDs (Cloudflare namespace ID, Fastly
  store ID) live in each platform's native manifest (wrangler.toml,
  fastly.toml), not in edgezero.toml. provision writes them there;
  config push reads them back.
- RequestContext store accessors become id-keyed:
  ctx.kv_store("id") / ctx.kv_store_default() (and similarly for
  config_store / secret_store). Each adapter builds a StoreRegistry<H>
  at request setup from [adapters.<self>.stores.*].
- Manifest validator enforces: ids non-empty; default in ids;
  every adapter has a name mapping for every id.

Naming:
- Field on the per-adapter block is `name` (matches the user's example),
  not `binding`. The Cloudflare wrangler.toml term `binding` is now
  called out as wrangler's terminology, not ours.

Secret references (§6.7):
- The string a #[secret] field holds is an app-defined reference; the
  spec documents both valid runtime patterns (logical store id or key
  within the default secret store). Validate just confirms the string
  is non-empty and that the app has a secret store available.

config validate (§11) explicitly covers app-config validation:
- TOML syntax, [config] table presence, type matching against C,
  serde-rejected unknown fields, validator business rules, non-empty
  secret references, and the manifest-side cross-checks.

Sub-project count: 7 → 9 (added schema rewrite + RequestContext API
rewrite as #2 and #3; existing app-config/validate/auth/provision/push/
polish become #4-#9).

This is a breaking change to the on-disk manifest schema; the in-tree
example/app-demo is migrated as part of the work, and a migration guide
ships with sub-project #2.
…cret forms

HIGH severity fixes:
- Cloudflare config store rewritten from [vars] to KV (§6.9) so
  `config push` actually reaches the runtime without redeploying.
  Lands in sub-project #3 alongside the rest of the runtime work.
- Sub-project #2 is now purely additive on the schema: no runtime
  changes, no removal of [stores.config.defaults]. The runtime bridge
  and the defaults removal move out of #2 (into #3 and #9 respectively).
- Spin completeness: validator skips adapters without an
  [adapters.<X>.stores] section. App-demo's Spin adapter omits stores
  until the in-flight Spin stores PR lands.
- Extractor design (§6.8): existing Kv / Secrets extractors keep
  working as default-store accessors; new KvNamed<const ID> /
  SecretsNamed<const ID> extractors give type-safe named access. No
  handler-facing break.
- Hooks, ConfigStoreMetadata, and app! macro added to sub-project #3
  scope; they all become id-keyed. Multi-store rewrite is now complete.

MEDIUM severity fixes:
- Validate bound is DeserializeOwned + Validate + AppConfigMeta (no
  Serialize). The serde_json::to_value object check is push-only;
  push adds Serialize.
- Secret semantics: two explicit forms via attribute. #[secret] = key
  inside the default secret store. #[secret(store_ref)] = logical store
  id in [stores.secrets].ids. Validate cross-checks the latter.
- AppConfigMeta::SECRET_FIELDS is now &'static [SecretField] carrying
  SecretKind so the CLI can apply the right validation per field.
- #[secret] constrained to non-flattened, non-renamed scalar fields;
  combinations with #[serde(flatten)] / rename / skip produce compile
  errors. Macro tests cover the constraints.
- Unknown-field rejection is no longer a validate guarantee; the
  generator template emits #[serde(deny_unknown_fields)] on the
  generated config struct so new projects opt in by default.
- Every public *Args derives Default + #[non_exhaustive]; external
  construction documented as Default + field mutation.

LOW severity fixes:
- Macro example fixed: #[proc_macro_derive(AppConfig, attributes(
  secret))] in edgezero-macros/src/lib.rs directly. No bogus _impl
  re-export.
- Cloudflare-invalid JS-identifier `name` values are errors (would
  break worker deploy), not warnings.

Sub-project ordering and risk:
- #2 risk dropped to L (purely additive).
- #3 grows to absorb Cloudflare KV swap + Hooks/macro/extractor.
- #9 now also drops [stores.config.defaults] and wires axum dev-server
  to seed from <name>.toml.
HIGH severity fixes:
- ConfigStore::get becomes async (#[async_trait(?Send)]). Cloudflare
  config moves [vars] -> KV with real async reads. Cascade (trait, 3
  adapter impls, Hooks, handlers, extractors) contained to #3.
- Drop const-generic &'static str extractors (don't compile on stable
  1.95). Kv / Secrets extractors refactored to yield a registry handle
  with default() / named(id) accessors.
- Introduce BoundKvStore / BoundConfigStore / BoundSecretStore so
  runtime accessors return a handle bound to the resolved platform
  name; callers just .get(key).await.
- Sub-project #2 models logical store declarations as
  Option<LogicalStoreConfig> so old-shape manifests (None) are
  distinguishable from new-but-incomplete ones (Some with empty ids).
  Keeps #2 genuinely additive.

MEDIUM severity fixes:
- Fastly native-manifest writeback: spec commits to a read/write-path-
  agreement contract; exact fastly.toml sections pinned in #7's plan.
- Adapter store completeness uses an explicit
  STORES_SUPPORTED_ADAPTERS allowlist (axum, cloudflare, fastly). A
  supported adapter omitting [adapters.<X>.stores] is an error; only
  non-allowlisted adapters (spin) skip.
- All "default store" prose uses the resolved default id (explicit
  default, else single ids[0]).
- AuthArgs no longer derives Default (avoids a placeholder subcommand
  leaking into a real auth path). §6.11 documents which *Args get
  Default.
- config push gains explicit "validate passes, push serialization
  fails" test scenarios (non-object typed config, compound shapes,
  skip_serializing_if, Option::None, flatten).

LOW severity:
- Ship-gate wording: existing commands stay backwards-compatible
  rather than "edgezero --help unchanged" (false once auth/provision/
  config land).

New requirement - environment-variable override resolution (§6.10):
- load_app_config overlays env vars on the toml [config] table.
- Env var format: <APP_NAME>__<SECTION>__..__<KEY>; __ separates every
  nesting level; APP_NAME is [app].name uppercased, hyphens to
  underscores.
- Type coercion against the target TOML type; --no-env escape hatch on
  validate and push.

app-demo (§15) now explicitly exercises every new capability: multi-
store, async config, named-kv extractor, nested config section, env
override, both secret forms, validate/push, auth/provision via mock.
…n, Fastly contract

HIGH severity fixes:
- Manifest old-vs-new discrimination corrected. Existing manifests
  already have [stores.kv/secrets/config] tables, so table-presence
  can't discriminate. Sub-project #2 now uses compatibility structs
  carrying legacy fields (name, legacy adapters) plus new logical
  fields (ids, default) side by side; the discriminator is
  ids.is_some(). The current app-demo edgezero.toml parses unchanged.
- Hooks cannot return bound handles. Hooks / ConfigStoreMetadata are
  static compile-time app metadata; bound handles need per-request
  adapter state. Split: Hooks/app! emit store metadata registries;
  only RequestContext returns Bound*Store handles. Adapters consume
  the metadata at request setup to build the runtime registries.
- Env overlay type coercion: with C: DeserializeOwned there is no
  pre-deserialization type reflection. Env vars now override existing
  keys only, coerced to the existing TOML value's type. Matches the
  current AxumConfigStore::from_env behavior. To make a key
  env-overridable it must appear in <name>.toml.
- Axum config push and runtime read agreed: the axum config store is
  backed by .edgezero/local-config-<id>.json; config push --adapter
  axum writes that file; edgezero dev regenerates it at startup. No
  more disagreement between push target and dev-server source.

MEDIUM severity fixes:
- Fastly writeback contract made concrete from Fastly's docs:
  [setup.<kind>_stores.<name>] + [local_server.<kind>_stores.<name>]
  keyed by resource link name (== our `name`). provision creates the
  store and ensures both fastly.toml sections exist; config push
  resolves the store id on demand via `fastly config-store list
  --json` (Fastly has no stable persisted id slot). Read/write paths
  all key off [adapters.fastly.stores.<kind>.<id>].name.
- Env key matching is deterministic and ambiguity-rejecting: keys
  transform to an env segment form (uppercase); two siblings mapping
  to the same segment is an AppConfigError. No case-insensitive fuzzy
  fallback.
- Cloudflare KV eventual consistency: §6.9 no longer claims values are
  live "on the next request"; CI does not assert immediate global
  Cloudflare visibility.

LOW severity:
- BoundSecretStore keeps the existing bytes::Bytes API (get ->
  Option<Bytes>, require_str), not Vec<u8>.
…e-PR delivery

Hard cutoff (per user directive — projects fully migrated, no compat):
- Removed all old-vs-new manifest discrimination: no compat structs,
  no ids.is_some() check, no legacy-field parsing. The store schema is
  rewritten outright. Legacy fields (name, legacy adapters overrides,
  [stores.config.defaults]) are hard load errors pointing at the
  migration guide.

Spin as a first-class store-capable adapter (PR #253 baseline):
- Removed the "Spin deferred" non-goal. Spin participates fully.
- New §6.7 Spin store semantics: KV is label-backed multi-store with a
  max_list_keys cap; config and secrets are both spin_sdk::variables —
  a single flat namespace, lowercase [a-z0-9_] keys, no dots.
- Replaced the flat STORES_SUPPORTED_ADAPTERS allowlist with an
  adapter x kind capability matrix (Multi vs Single). Validation: if
  any target adapter is Single for a kind, [stores.<kind>].ids must
  have exactly one id (you cannot have two config stores if you also
  target Spin).
- §6.4 config key model: nested config flattens to dotted keys;
  canonical handler form is dotted; Spin config store translates
  . -> __ internally; config push writes platform-native key form.
- Spin wired into commit 2 (runtime registry, async ConfigStore now
  cascades across all FOUR adapters), commit 6 (provision: spin.toml
  writeback for key_value_stores / [variables] /
  [component.<name>.variables]), commit 7 (config push: Spin variables
  in spin.toml).
- provision now has explicit axum (no-op, prints local-store note) and
  spin (manifest writeback, no CommandRunner) contracts; config push
  is split per adapter — no universal native-resource-ID assumption.

Other review fixes:
- Default resolution made strict: `default` required when ids.len() > 1.
- Docs config path corrected to docs/.vitepress/config.mts (not .ts).

Delivery: one PR with eight commits (one per sub-project), not eight
PRs. CI gates the PR head; each commit should still build for
bisectability. Sub-project count stays at 8 (manifest+runtime stay
merged as the atomic commit 2).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant