Repository navigation
Conversation
Spec §15 + §6.7 require every Spin variable used by the component
to be both declared in `[variables]` and bound under
`[component.<component>.variables]`. The `app-demo` spin manifest
had bindings for the non-secret config keys (greeting,
feature__new_checkout, service__timeout_ms) and the smoke-test
variable, but the two `AppDemoConfig` secret-form fields were
missing entirely.
Adds:
- `[variables].api_token` declaration with `required = true` +
`secret = true`. `api_token` is the `#[secret]` field — its
value resolves through the Spin secret store, and the wasm
component is only allowed to read it because the variable
declares itself a secret. No `default` — the operator must
set `SPIN_VARIABLE_API_TOKEN=<value>` at run time.
- `[variables].vault` declaration with `default = "default"`.
`vault` is `#[secret(store_ref)]` — the value is a runtime
store id, not material to keep secret, so a default is fine.
Bound the same way as the other component variables for
consistency with the AppDemoConfig surface.
- Both bound under `[component.app-demo.variables]` so the
wasm component can actually read them.
This is the manifest-side gap from Stage 8's task list. Handler
and integration-test work follow in Stage 8.2.
Add `examples/app-demo/crates/app-demo-cli/tests/config_flow.rs` —
three tests that drive `edgezero-cli`'s typed entry points through
`AppDemoConfig` (the downstream-CLI surface this example exists to
exercise). The env-overlay path the plan also calls out is already
covered by `app-demo-core/src/config.rs::env_overlay_overrides_
nested_value`, so this commit adds only the missing pieces.
Tests construct an app-demo-shaped manifest + `app-demo.toml` in a
tempdir rather than pointing at the in-repo example, so a push
writeback can't corrupt the example checked into git. Each helper
builds args via `Default::default()` + field assignment because
`ConfigValidateArgs` / `ConfigPushArgs` are `#[non_exhaustive]` —
an out-of-crate test can't use the struct-literal form.
- `config_validate_strict_passes_against_app_demo_config` —
confirms the typed `--strict` validator passes against the
AppDemoConfig shape (catches drift between the struct and the
validator contract).
- `config_push_axum_writes_local_config_json_without_secrets` —
pushes via axum, asserts the generated `.edgezero/local-
config-app_config.json` contains greeting / feature /
service.timeout_ms and that BOTH `#[secret]` (`api_token`)
AND `#[secret(store_ref)]` (`vault`) fields are absent.
- `config_push_spin_dry_run_prints_translated_keys_and_
preserves_manifest` — pushes via spin --dry-run, asserts
spin.toml is byte-identical after the call (no half-written
manifest).
`tempfile` and `serde_json` (workspace deps) added to
app-demo-cli's dev-dependencies. The remaining Stage 8 work
(named-KV handler updates and the e2e demo-server lifecycle) is
deferred to a focused follow-up — both require meaningful
behavioural changes beyond what fits in a test-only commit.
Per Stage 8's plan task list, the demo handlers must exercise both
named KV ids declared in `[stores.kv]` — not just `kv.default()`.
The previous handlers all routed through `kv.default()`, which
resolves to `sessions` but never makes the multi-store surface
visible end-to-end.
Updates:
- `kv_counter` now resolves via `kv.named("sessions")?`. Counter
state is session-flavoured — the per-deployment visit count
naturally lives in `sessions`.
- `kv_note_put` / `kv_note_get` / `kv_note_delete` now resolve
via `kv.named("cache")?`. Notes are short-lived snippet
payloads that pair well with cache semantics, and routing
them through a distinct id from the counter is what makes
the multi-store dispatch observable.
- Error messages updated to name the actual missing store id
instead of "no default KV store registered".
The `context_with_kv` test helper used to insert a single
`KvHandle` into request extensions, hitting the extractor's
legacy fallback path that wraps the handle in a synthetic
single-id registry under "default". After the handler rewrite
that path can no longer service `.named("sessions")` /
`.named("cache")` lookups.
`context_with_kv` now builds a real `KvRegistry` containing both
named stores (each backed by its own `MockKv`) with `sessions`
as the default, inserts the cloned registry into request
extensions, and returns `(RequestContext, KvRegistry)` so tests
that need cross-request persistence (put-then-get,
put-then-delete) can share the same registry — `KvHandle` is
`Arc`-backed so a cloned registry shares the underlying store.
The two cross-request tests (`kv_note_put_and_get`,
`kv_note_delete_returns_no_content`) drop their dangling
`_handle` discards and reuse the registry directly. All 26
app-demo-core tests still pass.
The Status block was stale at "Stages 6–8 — pending. Stage 6 (provision command) is next." Brings it current with what shipped on the branch. Stage 6 (provision) — done across four commits, one per adapter: 9a0369b (trait + axum + CLI delegate + stubs), d905e42 (cloudflare wrangler shell-out + wrangler.toml writeback), 79a54b6 (fastly fastly-CLI shell-out + fastly.toml writeback), 0933440 (spin pure spin.toml editing). Notes the implementation choice that was open in plan task 6.1 — provision uses a dedicated `Adapter::provision` trait method rather than an `AdapterAction::Provision` variant, because the typed `ProvisionStores` + paths/dry-run signature doesn't fit `AdapterAction`'s `&[String]` shape. Stage 7 (config push) — done across four commits, mirroring the Stage 6 split: bc0a705 (trait + axum + raw + typed CLI), 74d596a (cloudflare wrangler kv bulk put), d852f3f (fastly config-store-entry create per key), 57c7eb3 (spin pure spin.toml editing — both [variables] and [component.*.variables] tables with `.→__` lowercase key translation). Calls out the same trait-method choice as Stage 6, and that the typed flow strips BOTH `#[secret]` and `#[secret(store_ref)]` top-level fields. Stage 8 — partially shipped. Documents the three sub-commits that landed task 8.1: 3d3f87c (spin.toml manual secret declarations — api_token with required + secret, vault with default, both bound under [component.app-demo.variables]), 9fdd1f4 (three typed-CLI integration tests in app-demo-cli), 26fddcc (handlers rewired to Kv::named("sessions") + Kv::named("cache") with context_with_kv rebuilt around a real KvRegistry). Notes the env-overlay path was already covered by app-demo-core's existing unit test, so 8.1 only added the missing CLI integration tests. Calls out the remaining Stage 8 work explicitly: task 8.1 step 2's e2e demo-server test (needs ephemeral-port + readiness + RAII-teardown lifecycle helper), task 8.2 (generator template upgrade to emit the full seven-command Cmd), task 8.3 (CI wiring), and task 8.4 (cli-walkthrough.md + doc audit). The individual Stage 6/7/8 task checkboxes further down the file are intentionally not touched — the Status block is the source of truth for what's shipped, and flipping every checkbox would churn 50+ lines without adding signal.
Plan task 8.2. The Stage 1 cli template only emitted the four
pre-Stage-4 commands (`Build`, `Deploy`, `New`, `Serve`) because
`auth`, `provision`, and `config` didn't exist yet. Now that
Stages 4–7 have shipped them, a freshly-scaffolded project must
expose the full seven-command surface or new users silently lose
access to every command landed in the last five stages.
Updates:
- `templates/cli/Cargo.toml.hbs` gains a path dep on
`{{proj_core}}` so the scaffolded CLI can reference
`{{NameUpperCamel}}Config` from its sibling core crate.
Without this dep the generated `use` line wouldn't resolve.
- `templates/cli/src/main.rs.hbs` is rewritten to mirror
`app-demo-cli/src/main.rs`:
* imports `AuthArgs` / `ConfigPushArgs` / `ConfigValidateArgs`
/ `ProvisionArgs` alongside the original four args types,
* imports `{{NameUpperCamel}}Config` via the *underscored*
core module name — `use {{proj_core_mod}}::config::...`,
not `{{name}}_core::...` which would render the invalid
`my-app_core` for `my-app` (plan task 8.2 step 2 warning),
* declares a nested `{{NameUpperCamel}}ConfigCmd` enum for
`Config(Validate|Push)`,
* dispatches `Config` to the **typed** entry points
(`run_config_validate_typed::<{{NameUpperCamel}}Config>` /
`run_config_push_typed::<...>`) — the whole reason a
downstream CLI exists.
New `assert_scaffold_cli_full_command_set` test asserts the
scaffolded `Cargo.toml` depends on the core crate; that
`<name>-cli/src/main.rs` imports every args type, brings in the
typed config via the underscored module name, lists all seven
Cmd variants + the nested ConfigCmd, and dispatches via the
typed `run_config_*_typed::<...>` functions. The opt-in
`generated_project_builds.rs::generated_workspace_compiles`
test (run with `--ignored`) confirms the rewritten template
compiles end-to-end across all four adapter targets — verified
locally before commit.
Plan task 8.1 step 2: prove `config push --adapter axum writes
the file AND a demo server returns greeting on /config/greeting`.
A full HTTP-server lifecycle (subprocess + ephemeral port +
readiness poll + RAII teardown) adds significant test machinery
for what is, for app-demo, a data-contract verification: the
JSON `config push` writes must be exactly the payload
`AxumConfigStore` reads back, and the demo's `config_get`
handler dispatched against that store must surface the value.
The HTTP transport layer is axum's responsibility and is covered
by the axum adapter's own contract tests (`tests/contract.rs`).
The new test exercises the contract end to end without a server:
1. `config push --adapter axum` against a tempdir manifest
(typed flow — strips `#[secret]` / `#[secret(store_ref)]`).
2. `AxumConfigStore::from_path(&local_config_path)` — the
SAME loader the axum runtime uses (so a JSON-format drift
between push and read-back fails the test).
3. Wrap in a `ConfigRegistry` keyed by `app_config`.
4. Build a `/config/greeting` request, dispatch
`app_demo_core::handlers::config_get` directly.
5. Assert status 200 + body == "hello from app-demo".
Two small enabling changes:
- `AxumConfigStore::from_path` promoted from private to `pub`.
Documented as the supported path for downstream integration
tests that load JSON written to a tempdir (the
`from_local_file` default reads `.edgezero/local-config-
<id>.json` relative to CWD, which is racy when concurrent
tests change directories). The existing `from_local_file`
behaviour is unchanged.
- `app_demo_core::handlers` flipped from `mod` to `pub mod`
so the e2e test can invoke `config_get` from outside the
crate. app-demo is an example, not a stable library, so
this carries no backwards-compat cost; the `app!` macro
already used `handlers` internally — the visibility change
is purely additive.
`edgezero-adapter-axum` / `edgezero-core` / `futures` added to
app-demo-cli's dev-dependencies. The handler tests inside
`app-demo-core/src/handlers.rs` continue to pass unchanged. The
`tests/config_flow.rs` count goes 3 → 4.
Plan task 8.3. `examples/app-demo` is excluded from the root
workspace (`exclude = ["examples/app-demo"]` in Cargo.toml), so
the existing CI gates — `cargo test --workspace --all-targets`,
`cargo fmt --all -- --check`, `cargo clippy --workspace
--all-targets --all-features -- -D warnings` — never touched it.
Three concrete consequences this commit closes:
- The end-to-end push→AxumConfigStore→handler roundtrip Stage
8.5 added in `app-demo-cli/tests/config_flow.rs` would have
silently rotted (the test exists but never ran in CI).
- The Stage 8.3 named-KV handler tests in `app-demo-core`
were in the same blind spot.
- The app-demo `Cargo.lock` could drift without anyone noticing
until a contributor tried to build the example locally.
Adds three steps total:
- `test.yml`: "Run app-demo workspace tests" after the
generated-project compile check (`working-directory:
examples/app-demo`, `cargo test --workspace --all-targets`).
Kept on the existing test job (not the wasm matrix), per
plan §8.3 — axum-only, no live external calls.
- `format.yml`: "Run cargo fmt (app-demo workspace)" and
"Run cargo clippy (app-demo workspace)" after the
corresponding root-workspace steps, same `working-directory`.
App-demo carries the same strict-clippy config as the root
workspace, so this catches drift in the showcase a new user
actually clones.
`scripts/run_tests.sh` is intentionally untouched — it's a
developer-local convenience script not invoked from CI, and the
plan's task 8.3 wording is "Modify: `.github/workflows/test.yml`
(or `scripts/run_tests.sh`)". The CI workflows are what enforce
coverage.
Closes plan task 8.4. Five additive changes that together
finish Stage 8:
- **New `docs/guide/cli-walkthrough.md`** — the full myapp
loop in one page: scaffold → auth → provision → validate
→ push → build → deploy, with per-adapter behaviour for
each command, the env-overlay example, the Spin manual
secret-variable section, and end-to-end recipes for
Cloudflare / Spin / local-axum. Companion to
`cli-reference.md` (which catalogues each command
exhaustively) — the walkthrough tells the story.
- **`.vitepress/config.mts` sidebar update** — adds the
walkthrough and the existing `manifest-store-migration.md`
(which had no sidebar entry) under "Reference".
- **`cli-reference.md` build fix** — Stage 7.4 added a row
to the per-adapter `config push` table containing the
literal Spin template syntax `{{ <key> }}` inside inline
code. Vue's template compiler treats `{{ … }}` as an
interpolation expression even inside `<code>` blocks
inside markdown table cells, so VitePress's `npm run
build` has been failing since `57c7eb3`. The docs lint
job covers prettier + eslint only, so the regression
didn't surface in CI. Fix: HTML-escape the literal braces
(`{{ <key> }}`). The walkthrough uses
the same pattern wherever the template syntax appears in
inline code; fenced ```toml``` blocks need no escaping.
- **`roadmap.md` stale-ref fix** — "hot reload for
`edgezero dev`" referenced the pre-rewrite `dev`
subcommand name. The CLI now uses `serve --adapter axum`
for local dev (`dev` is reserved for a future
dev-workflow command, per spec). Reworded to point at the
current command + link to the CLI reference.
- **`.prettierignore` adds `superpowers/`** — the
handwritten specs and plans use prose-style continuation
indentation that prettier mangles (it rewrites lines
starting with `[[…]]` as link references, eating the
leading whitespace). Mirrors VitePress's existing
`srcExclude: ['superpowers/**']`. Without this entry,
every plan-status update from here on would either be
fought by prettier or include unrelated reformatting
churn. The 8.1 plan-status commit (`723dafe`) already
suffered from this — the next plan update would have hit
the same problem.
Audit findings: the only real stale ref in the public docs
was the roadmap `edgezero dev` mention. The `[stores.*]` /
old-singular-store-API hits were all in
`manifest-store-migration.md` (intentional historical
context), `kv.md` (links to the migration guide), and
`adapters/axum.md` (explains the removed `[stores.config.
defaults]`). Pre-built `.vitepress/dist/` artifacts contain
the old text but those are build outputs, not sources.
Full gate: `cargo test --workspace`, both clippy gates,
`npm run build`, `npm run format`, `npm run lint` — all
green. The opt-in `generated_project_builds.rs` test from
Stage 8.4 also passes (it depends on the Stage 8.4 template
updates, not Stage 8.7's docs).
Review finding (High): typed config push did not run the
strict/shared preflight it promises. `run_config_push_typed`
loaded typed config and ran typed secret checks, but never
called `run_shared_checks`; `load_push_context` explicitly
created `ConfigValidateArgs { strict: false }`. The raw flow
had the same gap.
The skipped checks include:
- per-adapter shared checks (`validate_app_config_keys`,
`validate_adapter_manifest`) — Spin's `^[a-z][a-z0-9_]*$`
key syntax and `[component.*]` discovery.
- `--strict` capability-aware completeness (rejects multi-id
stores against Single-capable adapters).
- handler-path well-formedness.
Spec §13: "strict pre-flight validation; load app-config".
This matters most for Spin (Single-capable for config and
secrets), where pushing a manifest with multi-id stores would
silently accept invalid config in the preflight and only
surface at the per-adapter writeback — potentially after a
partial mutation in an adapter without a belt-and-braces
adapter-side guard.
Fix:
- `load_push_context` synthesises `strict: true`.
- Both `run_config_push` and `run_config_push_typed` call
`run_shared_checks(&ctx.validation)` before flattening +
dispatch.
Coverage: two regression tests
- `raw_push_runs_spin_key_validation_before_push` — dashed
config key against a Spin-targeted manifest must fail
inside `run_shared_checks`, not inside Spin's
push_config_entries belt-and-braces guard.
- `typed_push_runs_strict_capability_completeness_before_push`
— a Single-capable adapter declared alongside multi-id
stores must fail the `--strict` capability completeness
check, even if the push targets a different adapter.
App-demo integration tests pass unchanged — the existing
fixture is single-id `[stores.config]` + `[stores.secrets]`
with axum + spin, which passes the strict preflight.
…canonicalisation
Review finding (Medium): `validate_typed_secrets` did
`value.replace('.', "__")` against `#[secret]` values and
checked the result against the config-key set. But the runtime
`SpinSecretStore::get_bytes` does NOT translate dots — it
lowercases the key before `variables::get`. So validation and
runtime diverge:
- Config key `greeting` + `#[secret]` value `"GREETING"`
passed validation (`greeting` != `GREETING`) but collided
at runtime (both resolve to Spin variable `greeting`).
- `#[secret]` value `"api-token"` passed validation entirely
but reached Spin at runtime with an `InvalidName` error.
The two runtime stores are asymmetric:
- `SpinConfigStore::translate_key` — `.→__`, case-preserving.
(Uppercase config keys are rejected upstream by
`validate_app_config_keys`, so by the time
`validate_typed_secrets` runs the config-key set is
guaranteed lowercase.)
- `SpinSecretStore::get_bytes` — `to_ascii_lowercase()`,
no dot translation.
Fix: `validate_typed_secrets` now canonicalises each secret
value via `to_ascii_lowercase()` (matching the secret-store
runtime exactly) before inserting into the collision-detection
set, and also runs `is_valid_spin_key` on the canonical form
so invalid Spin variable chars (dashes, digit-first values,
dots) fail validation rather than runtime. Config-key
canonicalisation is unchanged (`.→__`, case-preserving) —
they already passed `validate_app_config_keys` and are
guaranteed lowercase + dot-only at that point.
Coverage: three regression tests
- `validate_typed_secrets_detects_collision_after_lowercasing_secret_value`
— the `greeting`/`"GREETING"` case the review flagged.
- `validate_typed_secrets_rejects_invalid_spin_variable_in_secret_value`
— the `"api-token"` case the review flagged.
- `validate_typed_secrets_detects_collision_between_two_lowercased_secret_values`
— sanity check that the `seen` set still catches
secret-vs-secret collisions post-fix.
Review finding (High): hard-cutoff was not enforced for the runtime
store API. Per the spec intro:
> There is no backward compatibility with the pre-rewrite manifest
> schema or runtime store API.
But `RequestContext::{config,kv,secret}_store*` still silently fell
back to the legacy bare-handle accessors when no registry was
wired, and the `Kv` / `Config` / `Secrets` extractors synthesised a
one-id registry from a lone `*_handle()`. Either path masked
missing registry wiring — old handler code kept compiling AND ran
without anyone noticing the registry never got wired.
Fix is in two layers:
1. **Dispatcher boundary** normalises legacy bare-handle inputs to
single-id registries. New `StoreRegistry::single_id(id, handle)`
helper. Every adapter (axum service, fastly / cloudflare / spin
dispatch_core_request) now, before inserting handles into request
extensions, synthesises a one-id registry under the conventional
`"default"` id when the caller supplied a handle but no registry.
Both go into extensions — `ctx.kv_handle()` still works as the
explicit-opt-in escape hatch.
2. **Accessor + extractor** fallbacks removed. `ctx.kv_store*` /
`config_store*` / `secret_store*` and `Kv` / `Config` / `Secrets`
extractors now strictly require a registry. A bare handle in
extensions without a registry yields `None` (accessor) / Err
(extractor) — a missing registry is a real bug, not a
transparent legacy upgrade. The private `single_id_registry`
helper in `extractor.rs` is gone; the public
`StoreRegistry::single_id` is the only synthesis path and it
runs at the dispatch boundary, not behind the extractor.
Combined effect: `with_*_handle` / `dispatch_with_*_handle` still
work as convenience wrappers (the dispatcher synthesises a
registry), but missing wiring no longer masks. Net behavioural
change for in-tree code: a registry is always wired when a handle
is wired, so the change is observable only in edge cases (e.g. a
test that bypasses a dispatcher and inserts a bare handle into
extensions directly).
Test updates:
- 3 RequestContext fallback tests + 3 extractor fallback tests
rewritten as regression tests for the new hard-cutoff
semantics ("registry-aware accessor must not auto-upgrade").
- 2 app-demo `config_get` test helpers (`context_with_config_key`
+ `context_with_unavailable_config_store`) updated to wire
`StoreRegistry::single_id` instead of bare
`ConfigStoreHandle`. The handler tests stay otherwise
unchanged.
The legacy `RequestContext::{config,kv,secret}_handle()` methods
remain as explicit escape hatches. They're not the masking surface
the review flagged; deleting them is a separate broader migration
(would require updating `dispatch_with_config_handle` callers in
fastly/cloudflare contract tests + axum service's internal dev-
server routes). Reserving that work for a future commit if the
spec intent requires full removal.
…ed keys + both tables)
Review finding (Medium/Low): Spin dry-run tests only asserted
non-mutation, not the printed translated keys and table mentions
the spec calls out for the e2e showcase.
Two strengthening passes:
1. **`edgezero_adapter_spin::cli::tests::push_dry_run_does_not_
edit_spin_toml`** hardened. Now exercises a multi-entry input
whose `.→__` translation isn't a no-op (`service.timeout_ms`,
`feature.new_checkout`), then asserts:
- the summary line names the count (`would write 3 Spin
variable`),
- the summary line references BOTH `[variables]` and
`[component.<c>.variables]` tables,
- every translated key (`greeting`, `service__timeout_ms`,
`feature__new_checkout`) appears in a preview line,
- NO dotted source key (`service.timeout_ms`, etc.) leaks
through — regression guard for the `.→__` step,
- the component-binding template (`{{ <key> }}`) appears
paired with each translated key.
2. **New `spin_dry_run_preview_lists_app_demo_translated_keys_
and_both_tables`** in
`examples/app-demo/crates/app-demo-cli/tests/config_flow.rs`.
Drives `Adapter::push_config_entries` directly with the
entries the typed CLI flow produces from `AppDemoConfig`
(secrets stripped, dotted keys), so the `Vec<String>` status
lines can be inspected without process-global logger surgery.
Asserts the AppDemoConfig-derived translated keys
(`greeting`, `feature__new_checkout`,
`service__timeout_ms`) and both spin.toml table references
appear, AND that the stripped `api_token` / `vault` fields
do NOT leak. The existing `config_push_spin_dry_run_*` test
keeps the CLI integration contract (typed flow dispatches
cleanly + spin.toml byte-identical) and gains a comment
pointing at this companion for the printed-content
assertions.
Adds `edgezero-adapter` to the app-demo workspace deps (was
missing — only the four `edgezero-adapter-*` registrants were
declared) and `edgezero-adapter` + `edgezero-adapter-spin` to
app-demo-cli's dev-dependencies so the test can resolve the
registered spin adapter via `adapter_registry::get_adapter`.
…guide
Review finding (Low): docs/tooling drift across three places:
- PR template omitted `spin` from the feature check, said WASM
targets were only Fastly + Cloudflare, and the manual-test
bullet referenced the pre-rewrite `edgezero-cli dev`
subcommand (renamed to `serve --adapter axum` since Stage 1).
- `scripts/run_tests.sh` only covered the workspace test +
Fastly-specific wasm — no spin compile check, no app-demo
workspace (which is excluded from the root workspace), no
feature-combination check.
- The manifest-store migration guide still said `config push`
is "landing in Stage 7" — Stage 7 shipped four commits ago
(commits 7.1 / 7.2 / 7.3 / 7.4).
Fixes:
- `pull_request_template.md`:
* `cargo check --features "fastly cloudflare spin"` (spin
added).
* WASM target list includes `wasm32-wasip1 (Spin)`.
* Added `cargo fmt --check` to the test-plan checklist
(was missing despite being a CI gate).
* Added `examples/app-demo` workspace test + docs build
bullets — both are CI gates Stage 8.6 wired but the PR
template hadn't been updated to match.
* Manual-test bullet points at `edgezero serve --adapter
axum` with a link to the cli-reference note about the
renamed `dev` subcommand.
* Checklist gains a "registry not legacy single-handle"
item — Stage 9.3 enforced the hard-cutoff and this
catches regressions at review time.
- `scripts/run_tests.sh`:
* `workspace` test gains `--all-targets`.
* Adds workspace feature-combination check
(`--features "fastly cloudflare spin"`).
* Adds Spin wasm32 compile check (matching the CI gate;
the contract test stays CI-only — needs wasmtime + the
pinned runner).
* Runs the `examples/app-demo` workspace tests in a
subshell so the in-repo example stays validated locally.
- `docs/guide/manifest-store-migration.md`: the
"What about local config-store seeding?" section now points
at `edgezero config push --adapter axum` with a link to the
CLI reference, instead of "Stage 7 is coming". `config push`
has been shipped + documented for a while.
Review finding (High): runtime store API still had backward
compatibility surfaces. `RequestContext::{config,kv,secret}_handle()`
existed and adapter dispatchers still inserted bare handles into
request extensions, so pre-rewrite handler code kept compiling
and running. Spec §intro is unambiguous:
> no backward compatibility with the pre-rewrite manifest schema
> or runtime store API
Stage 9.3 closed the masking surface (accessor + extractor
fallbacks) but explicitly deferred the full removal. This commit
finishes the cutoff.
Removed:
- `RequestContext::config_handle()` / `kv_handle()` /
`secret_handle()` and their imports + dedicated tests
(the registry-aware accessors cover the same present/absent
contract).
- Bare-handle insertion into request extensions in all four
adapter dispatchers (axum `EdgeZeroAxumService`, fastly
`dispatch_core_request`, cloudflare `dispatch_core_request`,
spin `dispatch_with_handles`). The synthesis step that Stage
9.3 added — convert a wired bare `KvHandle` /
`ConfigStoreHandle` / `SecretHandle` to a one-id registry
under the conventional `"default"` id — still runs, so the
`with_*_handle` / `dispatch_with_*_handle` convenience APIs
keep working as before from the caller's perspective. The
only difference: nothing in extensions exposes a bare
handle.
- Two Stage 9.3 hard-cutoff regression tests
(`kv_store_returns_none_when_only_legacy_handle_wired`,
`secret_store_default_returns_none_when_only_legacy_handle_
wired`) lose their "ctx.*_handle() still works" assertion
branch — those methods no longer exist. The "bare handle in
extensions yields None from the registry-aware accessor"
contract is still asserted.
Migrated to registry-aware accessors:
- Axum `service.rs`: 3 internal demo routes
(config/kv/secret store wiring tests) +
`service_without_*_handle_still_works` smoke tests.
The secret-handle test fixture's keys move from
`"env/<key>"` to `"default/<key>"` to match the platform
name the dispatcher's synthesis pins.
- Axum `dev_server.rs`: 9 callers — mass `s/kv_handle/
kv_store_default/g`.
- Fastly / cloudflare / spin contract tests
(`config_value`, `config_presence`, `kv_value`,
`secret_value`): `ctx.config_handle()` → `config_store_
default()`, `ctx.kv_handle()` → `kv_store_default()`,
`ctx.secret_handle()` → `secret_store_default()`. The
spin `secret_value` handler's two-arg
`handle.get_bytes(store_name, key)` becomes single-arg
`bound.get_bytes(key)` — `BoundSecretStore` bundles the
store name.
Net effect: pre-rewrite handler code referencing
`ctx.{kv,config,secret}_handle()` now fails to compile, which
is exactly what the spec's hard-cutoff requires. The
`dispatch_with_*_handle` adapter wiring APIs and `with_*_handle`
axum service constructors stay public — they take legacy bare
handles as input but route them through the registry path
internally.
Full gate green: 350 core tests (down from 356, six dedicated
legacy-handle accessor tests removed), 25 fastly tests, 28
cloudflare tests, 73 spin tests, 26 app-demo-core tests, 5
app-demo-cli integration tests, both clippy gates, feature
combos, spin wasm32 check.
Docs / plan status / CLAUDE.md refresh land separately in
Stage 10.2.
Companion to Stage 10.1. The second-pass review flagged three docs/agent-instruction drift sites that contradicted (or hadn't caught up with) the hard-cutoff. Each is now consistent with the post-10.1 code. - `docs/guide/manifest-store-migration.md`: the "What this means for handler code" section used to say the pre-rewrite no-arg accessors were "preserved as legacy single-handle helpers and continue to work". They don't — they're gone per spec hard-cutoff. The section now explains the migration is mechanical (`s/_handle/\_store_default/`, plus the `BoundSecretStore::get_bytes(key)` single-arg shape) and documents that adapter setup-side `with_*_handle` / `dispatch_with_*_handle` constructors stay public — they internally synthesise a one-id registry under `"default"`, so setup code keeps working while handler code gets the hard-cutoff. - `docs/superpowers/plans/2026-05-20-cli-extensions.md` Status block: Stage 8 flipped from "partially shipped" to "shipped" (with commit hashes for plan tasks 8.1 / 8.2 / 8.3 / 8.4 + the e2e roundtrip in 8.5 + the noted intentional deferral of the full HTTP-subprocess lifecycle). New entries summarise the five Stage 9 review followups (9.1–9.5) and Stage 10.1's full hard-cutoff. The deeper task checkboxes further down the plan stay untouched — the Status block is the source of truth. - `CLAUDE.md`: two narrow corrections the reviewer called out explicitly. The `edgezero-cli/` blurb listed only `new, build, deploy, demo, serve` — now it lists the full surface including `auth`, `provision`, `config (validate| push)`. The adapter-pattern section described `cli.rs` as "build/deploy commands" — now it describes the actual Stage 6/7 surface (auth via `Adapter::execute`, dedicated `provision` and `push_config_entries` trait methods, ctor self-registration). No code changes. `npm run format` + `npm run build` (docs) green; main + app-demo gates unchanged from Stage 10.1's green run.
Per-commit and spec-section tags ("Stage 9.3 hard-cutoff:", "Task 3.4
step 1", "§6.6", "per §13", etc.) tracked HOW the code got to its
current state — useful in commit messages and the internal plan, not
in source comments. They make comments brittle (a future change
under "Stage 11.x" mid-comment confuses readers), couple the code
to a doc the public won't see (the spec lives under
docs/superpowers/ which is gitignored), and add noise without
explaining what the code does.
Mechanical scrub across 26 files + targeted hand-edits where the
regex left awkward phrasing. The internal spec and plan docs
under docs/superpowers/ keep their original references (they
ARE the spec). Public docs under docs/guide/ are untouched —
they reference the spec sections as cross-links and the published
site needs those.
Comment content is otherwise preserved. The two pre-existing
"Hard-cutoff:" descriptors that previously prefixed
"Stage 10.1 hard-cutoff:" are kept as standalone descriptors
(they describe the actual invariant — "no fallback to legacy
handle" — not the commit that introduced it).
Full gate green: cargo fmt, both clippy gates, 788 workspace tests,
32 app-demo workspace tests, feature combos, spin wasm32 check.
- `find_config_store_id` now returns `ConfigStoreLookup` (Found / NotFound / SchemaDrift), so the caller distinguishes "store not provisioned yet" (operator runs `provision`) from "fastly CLI output schema drifted" (operator pins a known-compatible CLI version). Each variant produces a distinct, actionable error. - `push_config_entries` records each successfully-committed key before attempting the next. On failure, the error names what was committed (safe to skip on retry), what failed, and what was not attempted -- so re-running push doesn't blindly re-push the already-committed prefix. - `provision` wraps `append_fastly_setup` failure with recovery guidance: the remote store was already created, so the operator needs to either manually append the setup block or delete the orphan remote via `fastly <kind>-store delete --name=<id>`. - Dry-run push now previews each entry it would create alongside the resolve+publish header, so callers can eyeball the keyset before running for real. - `single_store_kinds` is explicitly overridden to `&[]` with an inline rationale for each `validate_*` no-op, so the rationale for fastly diverging from spin's overrides lives next to the impl block.
- `provision` now checks for an existing `[[kv_namespaces]]` entry with a real 32-char-hex namespace id BEFORE shelling out to `wrangler kv namespace create`. A re-run on an already-provisioned wrangler.toml is a clean skip with a status line that names the existing id; a fresh scaffold (where ids are placeholders like `local-dev-placeholder`) is treated as unprovisioned and the placeholder gets rewritten in place. Previously the wrangler shell-out fired unconditionally, orphaning every newly-created namespace on re-run. - `upsert_kv_namespace` replaces the previous pure-append helper: if a binding entry exists, its `id` is updated in place; else a new entry is appended. This is what lets provision replace the scaffold placeholder cleanly. - `push_config_entries` dry-run is now lenient when the binding is not yet provisioned -- emits a preview line with `<unresolved>` in place of the namespace id and points at provision, instead of erring. Real-run still errors loudly so we never silently push to a non-existent namespace. - Dry-run now previews each entry it would create alongside the header (matching fastly's shape). - `create_kv_namespace` error names version sensitivity so a wrangler CLI bump that changes output format surfaces as "pin a known-compatible wrangler version" rather than a bare "did not parse".
- `is_valid_spin_key` failures used to surface as
"does not match \`^[a-z][a-z0-9_]*\$\`" -- correct but unhelpful;
the operator still has to figure out WHICH bit of the rule
broke. New `spin_key_rule_violation` helper returns a
per-failure-mode phrase (digit-first, uppercase, stray punct,
empty) that gets spliced into the error so the operator sees
the actionable hint directly. Applied at all three sites:
push (translates config key), validate_app_config_keys,
validate_typed_secrets.
- `write_spin_variables` used to err with bare
"`variables` is not a table" / "[component.X] is not a table"
when the TOML doc had the slot as a non-table value. New
`not_a_table_error` helper wraps the message with migration
guidance ("Spin requires [variables] block syntax; if you
previously set `variables = ...` inline, replace it with
block form"). Applied at all five sites.
- Tests cover both helpers directly (per-mode diagnostics +
migration-hint phrasing).
- `AuthArgs` doc now explains WHY it has no `Default` impl by contrasting with the other `*Args` types in the same module: AuthSub is a required subcommand with no neutral variant, so a default-constructed `AuthArgs` would have no sensible meaning. - `AxumConfigStore::from_path` rustdoc now shows the expected JSON shape inline (flat object of `string -> string`, with dotted keys preserved verbatim and non-string values rejected). The shape was documented before only by reference to `config push --adapter axum`; callers reading the doc directly now see the schema without having to find the push code.
When an adapter uses the setup APIs (`with_kv_handle`,
`with_config_handle`, `with_secret_handle`), the handle is wrapped
in a `StoreRegistry::single_id` keyed under `"default"`. Handlers
should then be able to resolve it via:
- `ctx.kv_store_default()` (existing test covers this)
- `ctx.kv_store("default")` -- the named-lookup path
- and observe `None` for `ctx.kv_store("any-other-id")`
`with_kv_handle_synthesises_one_id_registry_under_default` in the
axum service tests asserts all three at once, end-to-end through
the service stack -- so the precedence guarantee documented in
`StoreRegistry::single_id` is verified at the adapter boundary too,
not just inside the core test for the registry type.
(The auth.rs / provision.rs inline-test follow-up was de-scoped:
the existing run_auth_* and run_provision_* tests in lib.rs cover
both flows end-to-end through the shared manifest_guard / env-
override fixture, and the modules themselves are pure dispatch
with no unit-testable logic that doesn't already pass through
that fixture.)
L2 (spin push invalid-key wording): the original hint said "run `config validate --strict` to surface this earlier"; rewording now explains that `config validate` -- typed OR raw -- runs the same Spin-variable check against the manifest, so the operator path is clear without needing the `--strict` flag specifically. L4 (registry-over-handle precedence test): add `kv_registry_wins_over_bare_handle_when_both_wired` to axum service tests. Previously the precedence rule was implied by two separate tests (synthesised one-id registry + bare handle returns None); now a single test wires BOTH a real `KvRegistry` (declaring only `sessions`) AND a bare handle on the same service, then asserts the registry's ids resolve to its store, the registry's own default resolves, and the bare handle's synthesised "default" id is NOT exposed (registry wins outright, not as a merge or fallback). L5 (execute is the last loose-typed trait surface): flagged inline in the `Adapter::execute` rustdoc as a long-term cleanup target (typed per-action parameter structs mirroring provision / push_config_entries). No behaviour change. L6 (AuthArgs non_exhaustive note): doc now also explains that `#[non_exhaustive]` is purely forward-compat scaffolding (no struct-literal construction it blocks today), reserving the option to add a non-Default field later without it counting as a SemVer break. Plus a comment in cloudflare provision noting why we deliberately do NOT cross-check the stored namespace id against Cloudflare's API on every re-run (M2 trade-off): the skip line names the id explicitly so the operator can verify it themselves and remove stale entries by hand if the namespace was deleted out-of-band.
…p + docs
H1 (scaffold strict validation): edgezero.toml.hbs now ships with
[stores.secrets] uncommented because the scaffolded AppConfig
declares a `#[secret] api_token`. Strict typed validation rejects
the field unless the manifest declares a secret store, so the
out-of-the-box scaffold flow (`edgezero new <name>` +
`edgezero config validate`) was failing. The comment block now
explains why secrets is uncommented and which kinds the operator
needs to uncomment as their handlers grow.
H2 (spin push inline-table writeback): write_spin_variables used
to call `as_table_mut()` on each existing `[variables].<key>`
entry, which returns None for inline-table values (`<key> =
{ default = "..." }`). Real-world spin.toml files and the
app-demo's checked-in manifest declare variables in inline-table
form, so push was failing with "is not a table" -- not a useful
error since the entry IS a table, just inline. The writeback now
matches on both `Item::Table` and `Item::Value(InlineTable)`,
preserving whichever shape the user chose and updating `default`
in place. A new unit test locks this in against an inline-table
fixture.
M (fastly half-setup repair): setup_block_present only checked
[setup.<kind>_stores.<id>], so a half-edited manifest with the
[setup.*] block present but [local_server.*] missing was treated
as "already provisioned" and the missing block never got
repaired. The probe now requires BOTH parents; append_fastly_setup
is idempotent per parent (it skips the present one and writes the
missing one) so re-running provision now repairs the partial
state. New test covers both half-present permutations.
L (axum service.rs handle setters): the docstrings for
with_config_store_handle / with_kv_handle / with_secret_handle
still pointed at `ctx.config_handle()` / `ctx.kv_handle()` /
`ctx.secret_handle()` -- accessors removed by the Stage 10.1
hard-cutoff. Docs now point at `*_store_default()` / the
extractors and call out the hard-cutoff explicitly.
L (plan doc): 2026-05-20-cli-extensions.md still described the
singular handle accessors as remaining "as fallbacks". Updated to
reflect the actual Stage 10.1 hard-cutoff: the accessors are gone;
the setup APIs synthesise a one-id registry instead.
…fold)
Cloudflare:
- H2: is_real_namespace_id rejects hex-shape sentinels with fewer
than 6 distinct hex chars (all-zeros, deadbeef-style). Real
Cloudflare-API ids effectively never collide with this guard
(P < 10^-9); hand-typed placeholders consistently do.
- H3: upsert_kv_namespace doc spells out that toml_edit drops the
trailing inline comment on the id line under replacement; sibling
fields and table-level decor are preserved.
- H4: doc explicitly states provision is NOT safe to run
concurrently against the same wrangler.toml; the orphan-namespace
hazard is named.
- H5: read_namespace_id now errors loudly when kv_namespaces exists
but is neither array-of-tables nor inline-array (e.g.
`kv_namespaces = "oops"`). New item_kind() diagnostic helper.
- H6: provision skip-line names the existing id AND the
`wrangler kv namespace delete` step needed to free the orphan
before re-provisioning.
- H7: upsert_kv_namespace treats NotFound as "start with empty
document" symmetrically with read_namespace_id, so a
`wrangler kv namespace create` followed by writeback against a
missing wrangler.toml no longer orphans the remote namespace.
- H8: tests cover sibling-field preservation under upsert,
hex-shape-sentinel rejection (all-zeros / deadbeef / boundary),
multi-id-line extract pinning, and the new non-array
read_namespace_id error.
- H9: find_namespace_id rejects placeholder ids (non-32-char-hex)
with a "did you run provision?" hint, so real-run push never
shells out to wrangler with `--namespace-id=local-dev-placeholder`.
Existing tests updated to use 32-char hex fixtures.
Fastly:
- MED1: setup_block_present doc states it only proves table
presence -- the fastly CLI is the authoritative deploy-time
checker for inner shape.
- MED2: create_fastly_store now treats "already exists" stderr
(and similar phrasings) as idempotent success. Without this the
documented recovery path for an append-failure ("manually append
setup blocks and re-run provision") would re-trigger the wrangler
create and fail at "already exists" forever. New
looks_like_already_exists helper with multi-phrasing tests.
- GAP: push_entries_with_committer extracted from push_config_entries
as a pure helper. Unit-tested for all-succeed, zero-entries,
middle-failure (with committed/failed/not-attempted counts), and
first/last-entry-failure boundary cases. The shell-out is
factored as a closure parameter so the partial-failure error
shape is testable without fastly on PATH.
- dispatch_with_secrets doc surfaces the platform-name binding:
the synthesised SecretRegistry binds to a Fastly Secret Store
literally named "default"; operators with a different name must
use dispatch_with_kv_and_secrets or the manifest-aware run_app.
Spin:
- H2: write_spin_variables handles inline-table form of
[component.<id>.variables] symmetrically with the [variables]
inline fix from fe79025. New test covers updating an inline
component binding in place.
- M1: not_a_table_error wording for variables.<key> (scalar leaf)
now points the operator at `key = { default = "..." }` rather
than the misleading `[variables.key]` block-form suggestion.
- M2/M3: spin_key_rule_violation gains a comment explaining
per-branch reachability (uppercase-first is reachable via
validate_app_config_keys / validate_typed_secrets but NOT via
push, which lowercases first). The catch-all is kept defensive
with a debug_assert so a future regex tweak surfaces the gap.
- L1: spin_key_rule_violation test now asserts exact diagnostic
strings per failure mode, so a branch reorder can't pass by
accident.
- L2: validate_app_config_keys now mirrors translate_key_for_spin
exactly (replace + lowercase) so the diagnostic names the
actual written form. Uppercase source keys are still surfaced
via an explicit source-key check, so a silent case-mismatch
between source and runtime is impossible.
- Coverage: new test pins the bare-scalar [variables].key error
wording, locking in the targeted M1 hint.
Scaffold:
- The scaffolded AppConfig no longer ships with a live #[secret]
field; demonstrating it without wiring a handler that calls
ctx.secret_store_default() was empty signaling. The api_token
field becomes a commented example with a longer explanation of
when to uncomment it. The matching [stores.secrets] block is
re-commented; name.toml drops the live api_token entry. The
fresh-scaffold flow now passes `edgezero config validate` AND
`cargo check` AND `cargo run` out of the box without operator
intervention.
- generated_project_builds.rs runs `edgezero config validate` on
the scaffolded project BEFORE `cargo check` so a manifest/config
drift surfaces as a fast, clear error instead of a compilation
cascade.
Cross-adapter:
- synthesise_store_registries extracted as a pure helper in each
of cloudflare/fastly/spin's request.rs. Unit-tested in each
adapter for: bare-handle wraps under "default", registry wins
over bare handle when both wired (no merge, no fallback), all-
None inputs return all-None, config+secret bare handles handled
symmetrically. Axum already had end-to-end coverage; this lands
parallel asserts in the other three adapters so the precedence
contract is locked in at every adapter boundary.
…named adapter The reviewer noticed that `edgezero provision --adapter spin --dry-run` silently accepted a manifest declaring two config ids -- even though `edgezero config validate --strict` correctly rejected the same shape. The strict-validate path runs `strict_capability_completeness` (which iterates every declared adapter); `run_provision` skipped it entirely and dispatched straight into the adapter's `provision` impl. So the mismatch only surfaced at runtime as a confusing "wrong store" miss. Fix: - Factor `enforce_single_store_capability(manifest, adapter_name)` out of `strict_capability_completeness`. The shared helper is gated only on the registry lookup -- unregistered (feature-disabled) adapters are a no-op, mirroring the existing skip in the strict path. Single_kinds_empty short-circuit avoids walking the manifest for fully-Multi adapters (fastly, cloudflare KV/config). - `strict_capability_completeness` now delegates per-adapter via the same helper -- no behaviour change for `config validate --strict`. - `run_provision` calls `enforce_single_store_capability(manifest, &args.adapter)` unconditionally (NOT --strict-gated) before dispatching: the capability mismatch isn't stylistic, the platform genuinely cannot honour the declaration. Tests: - `run_provision_spin_rejects_multi_config_ids_via_capability_gate` reproduces the reviewer's scenario: spin manifest with two config ids, dry-run, expect the same "Single-capable for config" error shape as `config validate --strict`. - `run_provision_skips_capability_gate_for_kinds_within_single_id_floor` sanity-checks the floor: ids.len() <= 1 must still dispatch cleanly (the existing PROVISION_MANIFEST fixture lands here). Note on the reviewer's stale findings: the cloudflare clippy + test regression and the fastly half-setup repair were already closed in 6a5d89e; the reviewer was looking at fe79025 + WIP. Both pass cleanly at current HEAD.
High: apply_environment was unconditionally calling `cmd.env(...)` for every `[environment.variables]` binding, which overwrote any matching parent env var the operator had exported. Per the plan, manifest `value` is a DEFAULT -- the parent env wins. Without the guard, `EDGEZERO__ADAPTER__HOST=parent_env edgezero build --adapter axum` would observe the manifest default instead of `parent_env`. Fix: skip `cmd.env(...)` when `env::var_os(binding.env)` is already set. Two regression tests pin both directions (parent set -> parent wins; parent unset -> manifest default fills). Medium: axum's `read_axum_project` was falling back from `EDGEZERO__ADAPTER__HOST` / `EDGEZERO__ADAPTER__PORT` to legacy `EDGEZERO_HOST` / `EDGEZERO_PORT`, with docs advertising it as back-compat. The core runtime stopped reading the legacy names in the env-config rewrite; the axum wrapper was silently reviving a path the rest of the codebase had cut, contradicting the hard-cutoff spec. Drop the fallback; update the configuration guide to call out that operators must rename their CI scripts to the canonical double-underscore form.
The reviewer noticed that the scaffold's documentation and the
app-demo comments showed the env-var overlay prefix in lowercase
source form (`{{name}}__...` / `app_demo__...`), but the runtime
normalises with `to_ascii_uppercase().replace('-', '_')` --
so `app-demo` becomes `APP_DEMO__...`. Setting the lowercase
form is silently ignored at runtime, which the reviewer
reproduced (`app_demo__SERVICE__TIMEOUT_MS=9000` left the value
at 1500; `APP_DEMO__SERVICE__TIMEOUT_MS=9000` took effect).
Fix:
- Add `env_prefix` to `ProjectLayout` and the `EnvPrefix`
Handlebars value, derived via `env_prefix_from_name` which
mirrors `edgezero_core::app_config::app_name_prefix` exactly.
- `name.toml.hbs` and `config.rs.hbs` now use `{{EnvPrefix}}`
in every env-var comment, with a worked example
(`{{EnvPrefix}}__SERVICE__TIMEOUT_MS=2500`).
- `examples/app-demo/app-demo.toml` and
`examples/app-demo/crates/app-demo-core/src/config.rs` switch
to `APP_DEMO__...` literals.
- Two new tests pin the derivation against the runtime's rule:
one with concrete inputs, one that loops over names and asserts
agreement with `to_ascii_uppercase().replace('-', '_')` so a
future runtime change would surface immediately.
…est relocation This is the close-out for the 18-item review batch covering 3 Critical/ Important findings, 6 Medium, and 9 Low (review #N+1 atop 2a3ad63...5cc742b). Important / structural: - Thread env-resolved PLATFORM names through `Adapter::provision` and `Adapter::push_config_entries`. The runtime resolves `EDGEZERO__STORES__<KIND>__<ID>__NAME`; provision/push were passing only logical ids, so `EDGEZERO__STORES__CONFIG__APP_CONFIG__NAME=prod_config` was silently ignored at create time and the runtime later looked up a binding the CLI never created. New `ResolvedStoreId { logical, platform }` rides through `ProvisionStores` and the push signature; every adapter writes the platform name into the per-platform manifest, with the logical id preserved for human-facing wording. Regression tests in cloudflare + spin assert that an explicit platform name lands in wrangler.toml / spin.toml. - Per-adapter strict-validate gating in `run_provision`. Provision now runs `enforce_single_store_capability` (was already there) AND `strict_handler_paths` AND the target adapter's `validate_adapter_manifest` -- so a malformed handler path or a broken spin.toml fails BEFORE expensive remote calls happen. Two new regression tests cover both gates. - Remove `run_app_with_logging` (fastly) and `open_default` (spin) legacy wrappers per the hard-cutoff. Both hardcoded `DEFAULT_KV_STORE_NAME`/`EDGEZERO_KV`. Updated doc references. - Relocate `run_auth_*` (2 tests) into `auth.rs` and `run_provision_*` (10 tests) into `provision.rs`, per the CLAUDE.md "colocate tests with implementation" convention. New `crates/edgezero-cli/src/test_support.rs` exposes `manifest_guard`, `EnvOverride`, BASIC_MANIFEST, PROVISION_MANIFEST as `pub(crate)` so the per-module test mods share one harness instead of forking. Medium / wording + parser: - `extract_namespace_id` (cloudflare): now anchors on `[[kv_namespaces]]` AND requires the value to be a real Cloudflare-shaped id (32-char lowercase hex with diversity floor). Previously the first `id = "..."` line anywhere in wrangler stdout won -- vulnerable to wrangler version drift. - `looks_like_already_exists` (fastly): now requires BOTH a conflict signal AND a store-kind reference (`kv-store` / `config_store` / `secret store`), so an unrelated 409 "Conflict" from a different fastly subcommand can no longer be misread as idempotent store-create success. New negative tests pin the regression. - `cli-walkthrough.md`: env-overlay example used the literal `APP_NAME__...` as if it were the prefix; now uses concrete `MYAPP__...` for the walkthrough app `myapp` and explains the derivation rule. - `env_prefix_from_name` test now calls the actual runtime `edgezero_core::app_config::app_name_prefix` (newly made `pub`) so a future drift in either side surfaces immediately. The prior test was tautological. - `assert_scaffold_app_config` now pins that `DEMO_APP__` (uppercase) appears in the rendered scaffold and that `demo-app__` / `demo_app__` (source-form lowercase) does NOT. Low / docs + tests: - Cloudflare hex-diversity rate updated (10^-9 -> 10^-13). - Spin `write_spin_variables` doc explicitly lists the outer-table block-form requirement so the limitation is discoverable. - Fastly idempotent-skip line now mirrors cloudflare's: names the delete steps the operator needs to force a fresh remote. - `synthesis_tests` rationale comments aligned across cloudflare / spin / fastly (BoundSecretStore platform-name binding). - `#[expect(missing_trait_methods)]` reasons across axum/cloudflare/ fastly now name `single_store_kinds` as the overridden one. - Scaffold README drops the `API_TOKEN` mention (no secret wiring in the no-secret-default scaffold). - `generated_project_builds.rs` now also runs `--strict`. - Spec + kv.md doc references to `[adapters.spin.stores.kv.<id>].name` and `EDGEZERO_KV` rewritten to the hard-cutoff `EDGEZERO__STORES__KV__<ID>__NAME` model. Critical / CI: - `cloudflare/src/cli.rs:399` `starts_with("[")` -> `starts_with('[')` (single_char_pattern clippy regression introduced by the anchor fix).
…ice builder
This commit closes the latest reviewer findings (empty platform-name
override flows through to wrangler/fastly calls; legacy EDGEZERO_*
default constants still exist in public + manual paths) and the
follow-up design change to collapse the dispatch_with_* variant
fan-out.
env_config: reject empty / whitespace-only / control-bearing override
values in `EnvConfig::store_name` and fall back to the logical id.
Without this guard an exported `EDGEZERO__STORES__CONFIG__APP_CONFIG__NAME=`
(blank or accidentally cleared) would flow into
`wrangler kv namespace create ""` / `fastly config-store create
--name=` / blank TOML table names. Five new unit tests pin the
behaviour for empty, whitespace, newline, NUL, and the
realistic-punctuation acceptance path.
Hard-cutoff defaults: remove `DEFAULT_KV_STORE_NAME` /
`DEFAULT_CONFIG_STORE_NAME` / `DEFAULT_SECRET_STORE_NAME` from
`edgezero_core::manifest`. Drop the `Manifest::kv_store_name` /
`secret_store_binding` accessors that returned the constants when
no store was declared. Drop the fastly `DEFAULT_KV_STORE_NAME`
re-export and cloudflare `DEFAULT_KV_BINDING` re-export. Stop the
silent KV injection in `dispatch_with_config` / `dispatch_with_
config_handle` / `dispatch_with_secrets` -- KV is now wired
explicitly via `with_kv(name)` or not at all. Axum dev_server's
`kv_store_path` no longer special-cases the legacy `EDGEZERO_KV`
name to the bare `kv.redb` shortcut; every name gets a slug+hash
filename.
Service builder: collapse the per-store dispatch variants on
fastly + cloudflare into `FastlyService` / `CloudflareService`
builders. Per-request store wiring is the fluent form
`Service::new(&app).with_kv("name").require_kv().with_config("name")
.with_secrets().dispatch(req[, env, ctx])`. Each builder method is
independent; `require_*` promotes the previous `with_*` to required;
`with_config_handle` is the explicit-handle escape hatch. The
manifest-driven `run_app` is still the recommended entrypoint and
internally builds a Service. The legacy `dispatch_with_*` free
functions, `AppExt::dispatch` trait, fastly `dispatch_raw`, and
cloudflare `dispatch_raw` are gone.
Spec + plan: §6.9 added describing the Service builder + hard-
cutoff (no DEFAULT_*_STORE_NAME, no implicit KV injection); existing
§6.10-6.12 renumbered. Spec intro reworded to match the actual
portable-manifest reality (no per-adapter mapping block, only
logical ids + env-resolved platform names). Plan §11 calls out the
dispatch-variant consolidation.
Contract tests updated to use the Service builder; the
cloudflare router smoke tests dropped a meaningless
`.with_kv("kv")` that was carried over from the silent-default
shim. Fastly contract test renamed
`dispatch_with_config_handle_injects_handle` ->
`service_with_config_handle_injects_handle` for parity.
docs/.gitignore: add `.vitepress/.temp` so VitePress build
artifacts don't get accidentally staged.
The fixture-string literals `"sessions"` / `"app_config"` / `"default"` / `"cache"` / `"demo"` were repeated 10-30 times per test mod across the provision and push tests. The literals serve double duty as SETUP (passed into the production function) AND as ASSERTION INPUT (checked in dry-run output / error wording), so a silent typo that drifted both ends together would pass even though neither matched the production code. Hoisting them to module-level consts at the top of each test mod: - crates/edgezero-adapter-cloudflare/src/cli.rs: TEST_KV_ID = "sessions", TEST_KV_ID_ALT = "cache", TEST_CONFIG_ID = "app_config", TEST_SECRET_ID = "default". - crates/edgezero-adapter-fastly/src/cli.rs: TEST_KV_ID, TEST_CONFIG_ID, TEST_SECRET_ID (same shapes). - crates/edgezero-adapter-spin/src/cli.rs: TEST_KV_ID, TEST_KV_ID_ALT, TEST_CONFIG_ID, TEST_SECRET_ID, TEST_COMPONENT_ID = "demo". A few `find_config_store_id_*` test fixtures had the fixture JSON as a raw string with the const-named id embedded; those switched to `format!()` so the JSON still parses with the const value interpolated. Axum cli.rs left as-is (only 2-3 fixture uses; the indirection cost exceeds the typo-prevention benefit).
17 of 20 tasks
20 of 23 tasks
ChristianPavilonis
added this pull request to stack #408
October 6, 2026 21:02
15 of 17 tasks
Preserve the outbound hard-cut contract while integrating opt-in reusable application lifecycles with fallible assembly and adapter-owned delivery. Address R5 with shared future-based batch orchestration and R6 with shared response-body policy. Add deterministic cutoff cleanup and lifecycle evidence, and align fixtures, CI, specifications, plans, guides, and changelog.
Validate finite connection, raw-head, field-count and idle/header limits at startup. Own a bounded connection pool, preserve application deadlines, and compact response headers before parser reuse. Publish audited allocation assumptions and align CLI defaults, demo, scaffold and lifecycle documentation.
Load immutable config snapshots under validated file, entry, payload, registry residency, and staging limits before binding. Migrate all adapters to shared ConfigValue returns and hash canonical JSON incrementally. Add native TCP lifecycle and allocation regressions; align the demo, generator, specs, plans, and changelog while preserving conservative hosted capability claims.
9 tasks
Run each logging probe in a separate temporary working directory so pre-bind KV initialization cannot contend between concurrent subprocesses. Preserve startup ordering and exercise the exact demo CI sentinel.
15 of 17 tasks
This was referenced Oct 13, 2026
Capture typed evidence before response header filtering, retain it across all adapter response paths, and fail closed when original header fidelity is unavailable. Hard-cut response construction and decomposition, cover native and hosted normalization paths, and align documentation and fixtures.
Retain hard-cut outbound APIs, adapter-owned lifecycles, Content-Type provenance, and exact audited dependency requirements. Integrate main's toolchain and dependency updates across the workspace, demo, fixtures, and generator. Verify Rust 1.98.1 workspace tests, strict native/WASM clippy, runtime contracts, generated applications, documentation, dependency audits, and deployment action contracts.
Integrate main's IABTechLab transfer and retain the PR's outbound guide links when resolving the README. Align remaining active documentation, generated README, and capability diagnostic URLs with the canonical repository and Pages site. Add regression tests for generated guide links and capability diagnostics. Preserve the hard-cut outbound APIs, adapter runtimes, lifecycle ownership, limits, clocks, and audited dependency pins unchanged.
This was referenced Oct 19, 2026
Open
Add paired outbound time and pending ownership primitives, original-coding and marker policy, and matching demo and generator examples. Repair Spin response-owned streaming progress and retain local runtime characterization with fail-closed evidence checks. Keep the failed Fastly readiness gate and remaining head-first, eager and hosted qualification work explicit.
Extend the existing coding fixture with direct response handoff and bounded binary TCP capture. Verify 24 downstream relay cases per adapter, representation fields, framing and origin receipts. Retain local runtime evidence and update the adoption checkpoint. Keep head-first/eager migration, hosted qualification and the failed Fastly readiness gate explicitly open.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the portable outbound HTTP and request-lifecycle contracts across core and the Axum, Cloudflare, Fastly, and Spin adapters.
start_batch_until, indexednext, explicitcancel, orderedcollect, and thesend_all_untilconvenience path.start_batch_untiltakes an observation cutoff: reaching it stops observation even when individual slots retain budget, while earlier cutoffs remain a supported partial-result operation.PlatformMetadatafacts for memory ceilings, live inbound request population, and host-ingress accounting, preserving source and explicit unknown reasons.Fits,Exceeds, and typedIndeterminateoutcomes; publishes canonical Axum, Cloudflare, Fastly, generic Spin, and Akamai Functions profiles.CompletedorCutoff. OnlyCutoffpermits unresolved slots in a successful result. Explicit adapter failure, premature driver EOF, and duplicate or out-of-range indices returnOutboundBatchFailure, preserving the precise error and every terminal slot collected before failure.MonotonicClock::deadline_afteranchors relative cutoffs to that clock. Equality expiry wins and backwards time fails closed.ResponseEgressCompletion, transferred directly throughPreparedIngress,ResponseEgressEnvelope, and the soleResponseEgressAttempt. Terminal state is stored before the response-scoped completion and global observer run, each exactly once and behind independent panic boundaries. No response-extension carrier or envelope extraction bypass remains.ResponseEgressCompletion::joincomposition so independent response-owned resources share one exactly-once terminal report, and hard-cuts detached normalized errors toSend { completion, deadline } | Abortwith request-start-relative bounded deadlines.AdmissionDecision::Abort. Axum closes the owned HTTP/1 connection before body polling; Cloudflare, Fastly, and Spin return a fixed provider error at their strongest available pre-response boundary and remain conservatively classifiedBestEffort.Hooks::configureplus core-ownedApp::build::<A>(platform). Successful hooks must preserve the complete adapter-selected metadata; core rejects changed facts instead of restoring them. Typed configuration failures propagate unchanged and stop serving before request handling. The generated scaffold keeps finite ingress policy with core-owned empty lifecycle tokens; the checked-in demo retains the full application-owned resource-composition example.AbortController, Fastly's low-levelstream_to_client, and Spin's WASI response/body/result writers.Reviewer correctness follow-up
Addressed R1–R4 in
92111839:Main merge and shared-policy follow-up
Addressed R5 and R6 in
91f4af38, merging main683202c6without restoring superseded APIs:OutboundBatch::from_futures; adapters retain preflight validation, exchange ownership, clocks, and terminal samples. Ready results remain retained, driver EOF/index invariants remain enforced, and cutoff releases pending futures before reporting termination. Fastly retains its distinct provider-handle driver.ResponseBodyPolicyandapply_response_body_policy, preserving declared-length checks, encoded limits, decoding/header rewriting, decoded limits, passthrough, and rechunk order. Transport reads, bodyless handling, deadlines, cancellation, and final buffering remain adapter-owned.Result<(), E>through a wrapper that prevents SDK-generated second responses.R7's dependency-audit-script deduplication remains deferred; dependency contract checks remain unchanged and pass.
Bounded native ingress and config follow-up
ConfigValuereturns. Axum constructors requireConfigStoreLimits;from_mapis fallible. Reads share payloads, and local config push updates require a server restart.serde_json 1.0.150, hash canonical JSON incrementally, and release extraction's raw handle before secret retrieval. A moving-reallocation malformed-root probe measured 36,428 requested peak bytes under a 100,060-byte staging cap, with zero retained bytes after rejection.Unsupportedand response egress remainsBestEffort. Local emulators and guest contract tests are not deployed runtime evidence.Bounded provider reads and lifecycle qualification infrastructure
FinalizationRegistrycleanup support. Generated and demo Workers explicitly enableenable_weak_ref, and regression tests inspect their TOML rather than assuming an exact one-flag string.try_opennow returns typedConfigStoreError.Downstream acceptance impact
Cloudflare
outbound-deadlinesandstreamed-upload-deadlinesareBestEffort, notNative. Downstream acceptance suites must use best-effort deadline wording and must not claim exact cancellation enforcement until deployed host-observed evidence exists.The response completion, ingress abort, and fallible application-assembly changes are intentional breaking changes. Consumers must provide a completion on every response-producing admission decision and propagate
App::build::<A>(platform)failures.Evidence boundaries
Capability declarations remain conservative where provider behavior is not proved:
Unsupportedon all adapters.Nativeon Axum andBestEfforton Cloudflare, Fastly, and Spin because finite host-side teardown is not proved or synchronously preemptible.Nativeon Axum's owned HTTP/1 connection andBestEfforton Cloudflare, Fastly, and Spin because provider transport reset/close is not directly observable.BestEfforton every adapter because no boundary proves end-client receipt; provider-specific blocking and teardown limits are documented.Unsupported; provider parsing/materialization, SDK copies, informational responses, trailers, allocator overhead, and other host-owned memory remain outside guest-visible caps.BestEfforton every adapter because provider operations are not proved cancellable within a finite wall-clock interval.BestEffortpending a deployed host-observed cancellation artifact. Fastly and Spin retain their separately documented deadline, phase-budget, and isolation limitations.Validation
scripts/run_tests.shcargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace --all-targetscargo check --workspace --all-targets --features "fastly cloudflare spin"