Repository navigation
Conversation
Aligns `config validate` with the build/deploy/serve/auth pattern: per-adapter validation rules now live in each adapter crate's `Adapter` impl, not in `edgezero-cli`. The CLI orchestrates by looping over `registered_adapters()` and calling trait methods — zero adapter-name strings outside the registry lookup itself. - `edgezero_adapter::registry::Adapter` gains four default-no-op validation methods: `validate_app_config_keys(keys: &[&str])`, `validate_adapter_manifest(manifest_root, adapter_manifest_path, component_selector)`, `validate_typed_secrets(config_keys, plain_secrets)`, and `single_store_kinds() -> &'static [&'static str]`. Parameters are primitive (`&[&str]`, `&Path`, `Option<&str>`), so `edgezero-adapter` stays decoupled from `edgezero-core`'s `Manifest` / `SecretField` types — the CLI destructures the rich types and hands each adapter a flat slice. - axum/cloudflare override only `single_store_kinds = &["secrets"]` (`#[expect(missing_trait_methods)]` documents the deliberate fall-through to the defaults). Fastly is Multi for every store kind, so all four defaults stand. Spin implements all four methods — key-syntax (§6.7 check 1), component discovery (check 3), config/secret namespace collision (check 2, typed-only, `KeyInDefault`-only), and `&["config", "secrets"]`. `is_valid_spin_key` + `collect_spin_component_ids` (free fns) moved from `edgezero-cli/src/config.rs` to `edgezero-adapter-spin/src/cli.rs`. - `edgezero-cli/src/config.rs` drops the CLI-internal `trait AdapterCheck`, the `struct Axum` / `Cloudflare` / `Spin` impls, the `ADAPTER_CHECKS` const, and `adapter_checks_for` iterator helper. Two new functions — `run_adapter_shared_checks` and `run_adapter_typed_checks<C>` — loop over `manifest.adapters.keys()`, look each up via `adapter_registry::get_adapter`, and invoke the trait. The CLI still owns `flatten_keys` (the dotted-path producer every adapter consumes), `typed_secret_checks` (manifest-shape `#[secret]` / `#[secret(store_ref)]` checks that don't need adapter knowledge), and the `--strict` handler-path checks. - `strict_capability_completeness` now driven by `adapter.single_store_kinds()` from the registry, replacing the CLI's hardcoded `is_single_store_adapter` `matches!` table. - Test split mirrors the new ownership. Adapter-internal helper tests (`is_valid_spin_key_*`, the three `validate_*` method unit tests + the `single_store_kinds` assertion) land in `edgezero-adapter-spin/src/cli.rs`. CLI tests keep the end-to-end orchestration shape (`spin_key_syntax_rejects_*`, `spin_component_discovery_*`, `spin_config_secret_collision_*`, `strict_capability_completeness_rejects_*`) — they exercise `run_config_validate` through the registered adapter and still pass without rewriting because the adapter dispatch is transparent. - `edgezero-adapter-spin` gains `toml` as an optional dep under the `cli` feature for the component-discovery parser. Gates: cargo fmt / clippy --all-features (-D warnings) / cargo test --workspace --all-targets (44 spin tests, 25 cli config tests) / cargo check --features "fastly cloudflare spin" / cargo check -p edgezero-adapter-spin --target wasm32-wasip1 — all green on both the root and `app-demo` workspaces. Ship gate: both `edgezero config validate --strict --manifest …/app-demo/edgezero.toml` (raw) and `app-demo-cli config validate --strict` (typed) exit 0.
First of four commits for `edgezero provision` (spec §12). This one
ships the trait extension, CLI wiring, and the axum impl (the
only trivial one). Cloudflare / fastly / spin land their real
implementations in follow-up commits.
- `edgezero_adapter::registry`: new `ProvisionStores<'stores>`
struct + `Adapter::provision(manifest_root,
adapter_manifest_path, component_selector, stores, dry_run) ->
Result<Vec<String>, String>` trait method with a no-op default.
Parameters are primitive (`&Path`, `Option<&str>`, slice of
`String` ids) — `edgezero-adapter` stays decoupled from
`edgezero-core`'s `Manifest` type, same as the validation
methods.
- `edgezero-adapter-axum/src/cli.rs`: real `provision` impl —
prints one human-readable note per declared store id explaining
the local-only backing (KV in-memory; config in
`.edgezero/local-config-<id>.json`; secrets from env vars) and
exits 0. `--dry-run` is identical to a real run; nothing to
actually perform.
- `edgezero-adapter-{cloudflare,fastly,spin}/src/cli.rs`:
explicit `Err("... not yet implemented; landing in a follow-up
commit")` stubs so the dispatch wiring is exercised today
without pretending to provision against real platforms. Each
stub's TODO comment names the upcoming work — wrangler
shell-out + `wrangler.toml` writeback, `fastly *-store create`
+ `fastly.toml` writeback, `spin.toml` `key_value_stores`
editing.
- `edgezero-cli/src/provision.rs` (new): five-step thin
delegate — load manifest, ensure adapter declared, look up the
registered `Adapter`, build `ProvisionStores` from
`manifest.stores.*`, call `adapter.provision(...)`, log each
status line. Mirrors `auth.rs`'s shape exactly.
- `ProvisionArgs { adapter, dry_run, manifest }` in
`args.rs`, `Command::Provision(ProvisionArgs)` in the default
binary, `Cmd::Provision` in `app-demo-cli`. Parse tests cover
`--adapter <name>` + `--dry-run`, and the missing-`--adapter`
rejection.
- 4 orchestration tests in `lib.rs`: axum apply + dry-run both
exit 0 against a multi-adapter fixture; unknown adapter errors
by name; the three stub adapters return
`"... not yet implemented"`. The orchestration tests will keep
passing as the stubs are replaced with real implementations —
the "not yet implemented" check moves out adapter-by-adapter,
the others stay.
- `docs/guide/cli-reference.md`: new `### edgezero provision`
section + per-adapter behaviour table with the four built-ins.
Gates: cargo fmt / clippy --all-features (-D warnings) / cargo
test --workspace --all-targets (65 cli tests, 4 new) / cargo
check --features "fastly cloudflare spin" / cargo check -p
edgezero-adapter-spin --target wasm32-wasip1 — all green on both
the root and `app-demo` workspaces. Ship gate:
`./target/debug/edgezero provision --help` lists `--adapter` /
`--dry-run` / `--manifest`; `app-demo-cli provision --adapter
axum` exits 0 with the local-only notes; `app-demo-cli provision
--adapter cloudflare` exits 1 with the explicit
"not yet implemented" message.
…riteback) Replace the cloudflare provision stub with the real implementation: for each declared KV / config id, shell out to `wrangler kv namespace create <id>`, parse the namespace id from stdout, and append a `[[kv_namespaces]] binding = "<id>", id = "<extracted>"` block to the project's wrangler.toml. Existing bindings with the same name are detected and skipped (idempotent across re-runs). Secrets stay runtime-managed via `wrangler secret put` — provision is a no-op for them. Dry-run skips the shell-out and the writeback. toml_edit (workspace) is added to the cloudflare adapter's `cli` feature for in-place editing that preserves formatting and comments. The append path requires `kv_namespaces` to be an array-of-tables (the form wrangler itself prints); inline-array layouts trigger a clear "convert it manually" error rather than a silent rewrite. Coverage: 10 new tests on the cloudflare side (parser, writeback, provision dry-run) plus a CLI dispatch test that drives the adapter without needing wrangler on PATH. Docs/cli-reference updated with the real flow and the prerequisite that `[adapters.cloudflare.adapter] .manifest` point at wrangler.toml.
…back) Replace the fastly provision stub with the real implementation: for each declared KV / config / secret id, shell out to `fastly <kind>-store create --name=<id>`, then append empty `[setup.<kind>_stores.<id>]` and `[local_server.<kind>_stores.<id>]` tables to the project's fastly.toml. The empty tables are enough to declare the resource link for `fastly compute deploy` and viceroy local dev; `config push` fills in entries later (spec §13). Idempotency is manifest-driven (same model as cloudflare): if the setup block is already present in fastly.toml, the id is skipped with no shell-out and no edit. Re-runs after a partial failure converge — the writeback runs unconditionally after a successful create, and the skip path bypasses both. Store IDs are not persisted in the manifest; `config push` resolves them on demand. toml_edit (workspace) is added to the fastly adapter's `cli` feature for in-place editing that preserves formatting and comments. The writeback creates the parent `[setup]` / `[local_server]` tables if absent and tolerates a missing fastly.toml on first run. Coverage: 11 new tests on the fastly side (3 setup_block_present, 5 append_fastly_setup, 3 provision orchestration including the already-declared skip path) plus a CLI dispatch test that drives the adapter without needing fastly on PATH. cli-reference.md's per-adapter table marks fastly as shipped with the prerequisite that `[adapters.fastly.adapter].manifest` point at fastly.toml.
Replace the spin provision stub with the real implementation: for each declared KV id, append the label to the resolved `[component.<component>].key_value_stores = [...]` array in spin.toml. No native CLI is invoked — Spin KV stores are runtime-resolved by the Spin runtime / Fermyon at deploy, so the only operator-side work is declaring the labels per component. Component resolution mirrors the rule already used by Spin's `validate_adapter_manifest` (spec §6.7): a single `[component.*]` resolves implicitly; multi-component manifests require `[adapters.spin.adapter].component = "<id>"`. A selector that doesn't match any declared component is an error. The labels are written via toml_edit so formatting, comments, and sibling fields survive the edit, and re-runs are idempotent on the label. Config and secret ids are intentionally NOT handled by provision (spec §6.7): the manifest carries only store ids, not app-config field keys or secret key names. `provision` prints one line per config/secret id pointing the operator at `config push --adapter spin` (for config variables) and to the manual `[variables] secret = true` / `[component.*.variables]` declarations Spin requires for secrets. This matches the spec wording and keeps provision focused on what it can know unambiguously. toml_edit (workspace) is added to the spin adapter's `cli` feature. Coverage: 12 new tests on the spin side (4 resolve_spin_component, 5 ensure_kv_label_in_component, 5 provision orchestration including the config/secrets out-of-scope rows) plus a CLI dispatch test that drives the adapter with a single-component spin.toml. The CLI's old "spin stub reports not yet implemented" test is replaced by the new dry-run dispatch test. cli-reference.md's per-adapter table marks spin as shipped.
Extend the Adapter trait with `push_config_entries(...)`: per-adapter
config-store push that takes a resolved store id and pre-flattened
`(dotted_key, string_value)` entries. Default returns Err so adapters
opt in by overriding; axum lands the real impl in this commit and
cloudflare / fastly / spin carry stubs that surface their target
implementation crate when called (Stages 7.2 / 7.3 / 7.4 land them).
The axum push writes the flattened payload as a flat
`string -> string` JSON object to `.edgezero/local-config-<id>.json`
— the same file `AxumConfigStore` reads back at runtime. Creates the
`.edgezero/` directory on first use; honours `--dry-run` by writing
nothing.
CLI plumbing follows the same raw vs typed split `config validate`
already uses:
- `run_config_push(args)` — raw flow. Loads the on-disk TOML via
the existing env-overlay-aware loader, converts to JSON, walks
the tree producing dotted keys with arrays JSON-encoded into
single values (spec §6.5). No secret filtering — the raw flow
has no `AppConfigMeta` to read `SECRET_FIELDS` from.
- `run_config_push_typed::<C>(args)` — typed flow for downstream
CLIs. Runs strict pre-flight validation (`validator::Validate`,
secret presence, store-ref membership, adapter dispatch), then
serialises `C` via `serde_json`, strips every `#[secret]` and
`#[secret(store_ref)]` top-level field, flattens, and pushes.
Wired into both binaries: the default `edgezero` binary dispatches
`config push` to the raw flow; `app-demo-cli` dispatches to
`run_config_push_typed::<AppDemoConfig>`. Store-id resolution falls
back to `[stores.config].default` when `--store` is unset; an
unknown `--store` errors with the declared id list.
Coverage: 4 axum tests (write file, dry-run no-op, dir creation,
empty entries), 13 CLI tests (raw + typed flows, secret stripping,
explicit store selection, missing `[stores.config]`, undeclared
adapter, default-id resolution, array JSON-encoding, dotted-key
flattening, dry-run, validator failure, cloudflare stub error), and
3 args parse tests. Docs/cli-reference.md gets a full `edgezero
config push` section covering both flavours and the per-adapter
status (axum shipped; cloudflare / fastly / spin coming soon).
Replace the cloudflare push stub with the real implementation: read
the namespace id from wrangler.toml (matched by `binding = <store_id>`),
write the pre-flattened entries to a temp file in wrangler's bulk
format (`[{"key": "...", "value": "..."}]`), and shell out to
`wrangler kv bulk put <tempfile> --namespace-id=<id>`. The temp file
auto-deletes on drop. Keys arrive already-dotted from the CLI;
cloudflare passes them through unchanged.
The namespace resolver tolerates both `[[kv_namespaces]]`
(array-of-tables, what `provision` writes and wrangler's own
post-create hint prints) and the inline-array form. A missing
binding or a missing wrangler.toml errors with a "did you run
`edgezero provision --adapter cloudflare`?" hint — the most common
cause is forgetting to provision first.
Dry-run resolves the namespace id (so misconfigured manifests fail
fast) but never invokes wrangler and never writes a temp file. A
zero-entry push is a no-op that still resolves the namespace and
prints a friendly status line.
tempfile (workspace) is added to the cloudflare adapter's `cli`
feature. Coverage: 10 new tests on the cloudflare side (4
find_namespace_id covering array-of-tables / inline-array / missing-
binding / missing-file, 2 bulk_payload shape tests, 4 push
orchestration tests including the dry-run / missing-manifest /
provision-hint / empty-entries paths) plus the CLI's existing
cloudflare stub test rewritten as a real dry-run dispatch test that
exercises the resolver. cli-reference.md's per-adapter push table
marks cloudflare as shipped.
Replace the fastly push stub with the real implementation: resolve
the platform config-store id on demand via `fastly config-store
list --json` (matched by `name = <store_id>`), then shell out to
`fastly config-store-entry create --store-id=<id> --key=<k>
--value=<v>` per entry. Keys arrive pre-flattened from the CLI
(dotted form, spec §6.4); fastly passes them through.
The provision flow doesn't persist the platform store id (spec §12)
so push re-fetches every time — that's the whole reason this
adapter takes the list-then-create route rather than reading the id
from fastly.toml. The JSON parser accepts both a bare array and the
`{"items": [...]}` envelope so this stays compatible across fastly
CLI versions. A missing match errors with "did you run `edgezero
provision --adapter fastly`?" — the most common cause.
Re-runs on entries that already exist will fail loudly (fastly's
`config-store-entry create` refuses to overwrite). That's a known
gap documented in cli-reference.md; operators can delete the entry
or use `config-store-entry update` manually until a follow-up adds
upsert semantics.
Dry-run skips both the resolver shell-out and the per-entry create,
so CI exercises dispatch without fastly on PATH. An empty entries
list is a no-op with a friendly status line — no shell-out either.
serde_json (workspace) is added to the fastly adapter's `cli`
feature. Coverage: 6 new tests on the fastly side (4
find_config_store_id covering bare-array / items-envelope / no-
match / malformed-JSON, 2 push orchestration covering dry-run and
empty-entries) plus a CLI dry-run dispatch test that drives the
adapter without needing fastly installed. cli-reference.md's
per-adapter push table marks fastly as shipped with the
already-exists gotcha documented.
Replace the spin push stub with the real implementation: for each
entry, translate the dotted CLI key to a Spin variable name (`.` →
`__`, lowercased per §6.7) and write BOTH `spin.toml` tables — the
application-level declaration `[variables].<key>` with `default =
"<value>"`, and the component binding
`[component.<component>.variables].<key>` with `<key> = "{{ <key>
}}"`. Without both tables the wasm component cannot read the
variable; per the spec, `config push` (not `provision`) declares
variables because only push loads `<name>.toml` and thus knows the
keys.
Component resolution mirrors Stage 6.4's KV writeback: a single
`[component.*]` resolves implicitly, multi-component needs
`[adapters.spin.adapter].component`, a selector that doesn't match
any declared id is an error. Writeback is idempotent via toml_edit
— re-running updates the `default` value in place and overwrites
the binding without duplicating entries. Formatting, comments, and
sibling fields are preserved.
A belt-and-braces check rejects any key whose translation violates
Spin's `^[a-z][a-z0-9_]*$` rule before touching spin.toml — `config
validate` should already have caught it, but the adapter-side
guard keeps spin.toml well-formed if a raw push slips an invalid
key through. Secret variables stay manual: the typed CLI flow
strips `SECRET_FIELDS` upstream, and the raw flow leaves
`secret = true` declarations to the developer (spec §6.7).
Dry-run prints the would-be edits without writing — including
both the count line and one preview line per variable showing the
translated key, default value, and component binding. An empty
entries list is a no-op with a friendly status line.
Coverage: 12 new tests on the spin side — 3 translate_key_for_spin
(dot translation, passthrough, lowercase normalisation), 4
write_spin_variables (both tables present, idempotent re-run,
preserves siblings, plus a §13 golden test asserting every written
variable name passes the Spin regex floor of the validation
ladder), 5 push orchestration (dry-run no-edit + count line,
real writeback round-trip including `.→__`, missing manifest
path, dashed-key rejection, empty-entries no-op). CLI gets a
spin dry-run dispatch test that drives the adapter with a
single-component spin.toml. cli-reference.md's per-adapter push
table marks spin as shipped — closing Stage 7 with all four
adapters owning their own push impl via `Adapter::push_config_entries`.
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.)
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Review
Reviewed 98930917d96cb665c7255f36ca8bbd61fc7539e1 → 5527b58e2a7cb6307d5c7a8926cd5dfc6f4f8b4a against the confirmed portable outbound HTTP and request-lifecycle intent, including intentional API replacements and conservative provider claims.
Requesting changes for R1–R4. R5–R7 are non-blocking maintenance suggestions. Comment prefixes follow the Code Review Emoji Guide: 🔧 requests a change; ♻️ suggests refactoring. All seven findings are inline. No verified style findings.
Verification
Workspace tests passed: 1,843 passed, one ignored generated-workspace test. Root and demo rustfmt, strict all-feature workspace Clippy, workspace feature compilation, Cloudflare/Fastly/Spin WASM compilation, docs format/lint/build, and outbound/dependency contract scripts passed. Initial offline attempts lacked cached dependencies; checks passed after fetching them. Focused scratch reproductions used actual core sources, the actual Axum response implementation, and locked Hyper 1.10.1.
Evidence boundaries
Four independent lane reviews plus coordinator verification covered selected production boundaries. This is a partial review of the 192-file change, not exhaustive approval. Manual coverage remains incomplete in generator/macros/demo, provider store and CLI paths, and substantial tests/specs. Generated-project acceptance, full feature matrices, provider runtime suites, and deployed cancellation behavior were not verified.
Axum terminal classification has raw-socket runtime counterexamples; batch failure classification has executed actual-core counterexamples. Exactly-once resource lifetime across every cancellation path remains unproven. The scratch harness did not exercise the full standard service/supervisor. Passing checks do not clear those remaining safety claims.
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.
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.
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"