Skip to content

EdgeZero CLI Extensions: extensible CLI, multi-store manifest, auth/provision/config - #269

Merged
aram356 merged 290 commits into
mainfrom
feature/extensible-cli
Jun 29, 2026
Merged

EdgeZero CLI Extensions: extensible CLI, multi-store manifest, auth/provision/config#269
aram356 merged 290 commits into
mainfrom
feature/extensible-cli

Conversation

@aram356

@aram356aram356 commented May 20, 2026

Copy link
Copy Markdown
Contributor

Epic: #268

Summary

Turns edgezero-cli into an extensible library, rewrites the manifest store schema and runtime to a multi-store model, adds auth / provision / config validate / config push commands, replaces the dev subcommand with a contributor-only demo, and updates app-demo to exercise everything across axum / cloudflare / fastly / spin. Plus two integrations completed in-branch: Spin 6.0 (WASI Preview 2) and chore/strict-clippy (PR #257).

Delivered as one PR, eight sequential stages (one per sub-issue of #268), with additional hardening from multi-round self-review and two integration cycles on top.

Stages (sub-issues of #268)

#Sub-issueStageStatus
1#260Extensible edgezero-cli library + generator + app-demo-cli skeleton✅ Landed
2#261Manifest + runtime rewrite (atomic, all four adapters)✅ Landed
3#262App-config schema, derive macro, env-overlay loader✅ Landed
4#263config validate command✅ Landed
5#264auth command + CommandRunner infrastructure✅ Landed
6#265provision command✅ Landed
7#266config push command✅ Landed
8#267app-demo integration polish + docs audit✅ Landed

In-branch integrations on top of the eight stages

Spin 6.0 (WASI Preview 2) — 487ac5f

  • spin-sdk = "6" workspace + app-demo + generator default
  • wasm32-wasip1wasm32-wasip2 for the Spin adapter only; Fastly stays on wasip1. CI matrix entry + CLAUDE.md + docs + scripts + scaffold template + app-demo manifests all updated.
  • 6.0 API rewrite: IncomingRequestRequest; req.into_parts() + IncomingBodyExt::bytes(); deleted the 10-arm Method enum match (Method is now a re-export of http::Method); Request::builder()…build().body(FullBody::new(Bytes::from(...)))?; async Store::open/get/set/delete/exists/get_keys; async variables::get; #[http_component]#[http_service].
  • Response type now Response<FullBody<Bytes>>; contract tests use http_body_util::BodyExt::collect().

chore/strict-clippy integration — 611064e

  • Merged origin/chore/strict-clippy so the workspace-wide strict-clippy gate (pedantic warn + restriction deny, 13-item allow-list) sits underneath everything in this PR.
  • Inherited the per-target adapter-wasm-clippy CI matrix added in Enable strict clippy with documented allow-list and defensive-coding pass #257; switched its spin entry from wasip1 → wasip2 to match the Spin 6 migration.
  • Conflict resolution: for the spin adapter (incoming was 5.2-era) and the cloudflare adapter (incoming pre-dated the Service builder), our 6.0 / Service-builder code wins semantically. Contract tests likewise stay on the Service builder surface. See merge commit body for the full per-file rationale.

Strict-clippy pass on wasm32 adapter targets — 910739d

  • Brings the inherited adapter-wasm-clippy matrix to green: cloudflare (~44 errors), fastly (~14), spin (~115).
  • Patterns applied (no #[allow] sprinkles): pub use mod::Foopub mod foo; absolute_pathsuse imports; min_ident_chars → renames (|e||err|, |c|char::is_control); #[inline] + # Errors on every public fn; arbitrary_source_item_ordering reordering; tests_outside_test_module + expect_used in tests fixed by wrapping in #[cfg(test)] mod tests { ... }; used_underscore_itemsconst _: fn() = assert_provider_impl::<T>;; let_underscore_must_usedrop(init_logger()); type alias SpinFullResponse = Response<FullBody<Bytes>> for the recurring complex type; HeaderValue::from_static("spin") to kill expect_used in proxy.rs; .saturating_add() for arithmetic_side_effects.

Self-review followups closed in this PR

Two close-out commits address findings from the post-merge self-reviews:

  • 16898f8 — Store id validation (rejects blank / whitespace / control / duplicate logical ids with store_id_blank / store_id_duplicate errors); docs ESLint ignore for .vitepress/.temp so the docs lint gate is build-state-independent; removed the misleading singular StoreDeclaration::config_store_name(&self, _adapter) helper.
  • 4928cdc — Spin wasm contract CI fix (tests insert *Registry::single_id(...) instead of bare handles so they match the dispatch boundary's synthesise_store_registries); generated_project_builds now invokes the generated typed CLI's config validate --strict (catches AppConfig drift the raw validator can't); docs build fix (multi-line inline backticks in cli-walkthrough.md push bullets converted to fenced shell blocks — VitePress was parsing the leaked <tempfile>/<id> as unterminated HTML tags); provision + push docs now describe the <platform-name> resolution (EDGEZERO__STORES__<KIND>__<ID>__NAME → logical id) for cloudflare/fastly instead of the wrong "matched by <store_id>" wording; Spin docs corrected on secret translation (config keys translate .→__; #[secret] values are only lowercased — matches SpinSecretStore::get_bytes + the cli validator exactly); stale plan doc references updated (app-demo IS in CI; Spin target IS wasip2); axum secret store rustdoc no longer references the removed cargo edgezero dev command.

Design + plan

Committed under docs/superpowers/ (force-added — that path is currently in .gitignore; maintainers can decide whether to formally track it):

  • docs/superpowers/specs/2026-05-19-cli-extensions-design.md
  • docs/superpowers/plans/2026-05-20-cli-extensions.md

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings (host strict-clippy)
  • cargo test --workspace --all-targets (1057+ tests pass)
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin
  • cargo check -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly
  • cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare
  • cargo clippy -p edgezero-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown --all-targets -- -D warnings
  • cargo clippy -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --all-targets -- -D warnings
  • cargo clippy -p edgezero-adapter-spin --features spin --target wasm32-wasip2 --all-targets -- -D warnings
  • cd examples/app-demo && cargo test --workspace --all-targets
  • cd examples/app-demo && cargo fmt --check && cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo check -p app-demo-adapter-spin --target wasm32-wasip2
  • cargo test -p edgezero-cli --test generated_project_builds -- --ignored (now also runs the generated typed CLI's config validate --strict)
  • cd docs && npm run lint && npm run format && npm run build
  • Spin wasm contract suite under wasmtime run (12/12 — reviewer-verified locally; CI matrix runs the same)

aram356 added 30 commits April 29, 2026 12:55
Reorder source items across edgezero-core and the adapter/cli crates
to satisfy the canonical clippy item ordering (ExternCrate → Use → Mod
→ Static → Const → TyAlias → Enum → Struct → Trait → Impl → Fn) with
alphabetical ordering inside each kind. Applies recursively to:
- top-level items in 12 core files (app, body, config_store, context,
error, extractor, http, key_value_store, middleware, params, proxy,
router, secret_store) and the adapter/cli files that needed it
- struct fields and constructor argument order
- enum variants
- methods inside `impl` blocks
- items inside `mod tests {}` blocks (including macro_rules! placement
before `use super::*` where required)
Pure reordering — no behavioural changes, no `#[expect]` annotations.
All clippy lints pass, 557+ tests green, all three wasm targets compile.
All cast sites turned out to be either redundant trait-object coercions
that Rust performs automatically, or numeric conversions that can use
a sibling const at the right type:
- spin/decompress.rs (2 sites): added MAX_DECOMPRESSED_SIZE_U64 sibling
const so the `Read::take` callsites do not need a usize→u64 cast
- fastly/logger.rs: replaced `Box::new(logger) as Box<dyn log::Log>`
with an inline `let boxed: Box<dyn log::Log> = Box::new(logger);`
pattern (Box<T>→Box<dyn Trait> coerces automatically through a typed
binding)
- core/middleware.rs (4 sites in tests) and core/router.rs (1 site):
same pattern — drop redundant `as BoxMiddleware` casts where the
surrounding `Vec<BoxMiddleware>` annotation already drives coercion
- cli/main.rs: drop `&[] as &[String]` — the function signature drives
inference
Workspace allow is gone; clippy + 557+ tests + all wasm targets pass.
Six arithmetic sites — all on usize/SystemTime where overflow is
practically impossible but the lint cannot prove it. Real fix: use
the explicit no-panic variant at each site.
- axum/key_value_store.rs: `limit + 1` → `limit.saturating_add(1)`,
`MAX_SCAN_BATCHES * LIST_SCAN_BATCH_SIZE` → `saturating_mul`,
`batch_count += 1` → `saturating_add`, and `SystemTime::now() + ttl`
→ `SystemTime::now().checked_add(ttl).ok_or_else(KvError::Internal)?`
so an absurd ttl propagates as an error rather than panicking
- core/key_value_store.rs (test MockStore): same `checked_add(ttl)?`
pattern so the test backend matches the production contract
- cli/generator.rs: `count + 1` → `saturating_add(1)`
Workspace allow gone; all clippy lints, tests, and wasm targets pass.
viceroy 0.17.0 raises its MSRV to rustc 1.95; the workspace ships rustc
1.91 (.tool-versions), so the unpinned `cargo install viceroy` started
failing with "rustc 1.91.1 is not supported by viceroy-lib@0.17.0
requires rustc 1.95". 0.16.x is compatible and is what local dev uses.
Matches the CI pin (`^0.16`) so local dev resolves the same major.minor
that CI installs. 0.17 raises MSRV to rustc 1.95 which is past the
workspace's rust 1.91.1.
Single source of truth: replace the hardcoded `^0.16` in the workflow
with a step that greps the version out of `.tool-versions`. Matches
the existing pattern used for rust, and means a future viceroy bump
is a single-line edit in `.tool-versions` rather than two places.
Single-character bindings, closure params, and helper variable names
were renamed to descriptive equivalents across 31 files. Common
patterns:
- closure error params: `|e|` → `|err|`
- closure key/value pairs: `|(k, v)|` → `|(key, value)|`
- short locals in tests: `let s = ...` → `let store/service/cs = ...`
- `Some(p)` for `&UserProfile` → `Some(found)` (avoids shadow with
outer `profile` var, which would trip `shadow_reuse`)
- `let h = handle.clone()` in concurrent tests → `let kv_handle = ...`
to avoid shadowing the outer `handle`
- `m` (manifest data) in dev_server.rs / main.rs → `manifest_data`
- HTTP closure params `|c| c.get(...)` → `|http_client| http_client.get`
No behaviour changes — pure renames. Workspace allow gone; clippy +
557+ tests + all wasm targets pass.
Investigated removing the allow: 40 sites in edgezero-core alone (every
public error type and handle: EdgeError, KvError, SecretError,
ConfigStoreError, ConfigStoreHandle, plus the entire Manifest* family).
The renames would force consumers in 4 adapter crates + cli + demo to
either write `kv::Error`/`secret::Error`/etc. at every callsite or set
up `use ... as KvError` aliases — a net loss in readability for a
deliberately-prefixed cross-crate API.
Replaced the terse comment with a longer one documenting the audit and
why the allow is load-bearing rather than a leftover.
Attempted the rename and surfaced three blockers:
1. `proxy::Request`/`proxy::Response` would collide with
`http::Request`/`http::Response` already imported at every
consumer; the only non-colliding alternatives (`OutboundRequest`,
`Outbound`) are strictly more verbose than `ProxyRequest`.
2. `manifest.rs` has 17 `Manifest*` types used directly by adapters,
cli, demos, scaffold templates, and the `#[app]` macro output.
Stripping the prefix would force every site to write
`use edgezero_core::manifest::Spec as Manifest` etc.
3. The macro emits code that references these names by their current
spelling; renaming requires regenerating every app and updating
CLAUDE.md examples.
The lint's intent (the std-style `module::Type` idiom) is sound but
fights this crate's flat re-export surface, and several names cannot
be deprefixed without losing meaning. Allow stays with the audit
documented inline.
Two sites in middleware.rs computed `start.elapsed().as_secs_f64() *
1000.0` to get milliseconds with sub-ms precision for the
request-logging line. Sub-ms precision in a log line is unnecessary —
switch to `Duration::as_millis()` (returns `u128`) and drop the
`{:.2}` format spec. No precision loss that any reader would notice;
removes the only float-arithmetic site in the workspace.
Audit: only `Body { Once, Stream }` triggers the lint workspace-wide.
Marking it `#[non_exhaustive]` would force `_ => unreachable!()` at
each of the 37 external match sites in the four adapter crates, and
a third Body variant would silently `panic!` at runtime instead of
producing a compile error at every consumer. Body is intentionally
closed; the lint is genuinely incompatible with the design.
Add `#[inline]` to every public function and trait method across the
workspace. Touches 44 files: edgezero-core (~242 sites) and the four
adapter crates. Placement is right above the `pub fn` after any doc
comments and `#[must_use]`. No `#[inline(always)]` — leaving the call
to rustc/LLVM, which is the actual inlining decision-maker.
Note: the original workspace-allow rationale ("rustc/LLVM make better
choices than us") is still half true — the lint just wants the *hint*
present, even though rustc inlines monomorphised generics aggressively
without it. Adding the hint is cheap and the lint is satisfied.
Defends against the CodeQL `rust/cleartext-logging` rule, which heuristically
flagged `log_store_bindings` because it pipes
`manifest_data.secret_store_name(adapter)` into `log::info!`. The method
returns the binding identifier from `edgezero.toml` (e.g. `"MY_SECRETS"`),
not the secret value — but the function name pattern triggers the
analyzer's "credential getter" heuristic. Renaming to
`secret_store_binding` makes the intent unambiguous and the alert no
longer fires. Also reorders the impl method block so
`secret_store_binding` lands before `secret_store_enabled` per
`arbitrary_source_item_ordering`.
GitHub deprecated Node 20 as the JavaScript actions runtime on
2025-09-19; v4 of these three actions still ships Node 20 and triggers
the deprecation warning on every CI run. v5 majors ship the Node 24
binary and the warning goes away. All three v5 majors are stable;
the bump is mechanical and covers test.yml, format.yml,
deploy-docs.yml, and codeql.yml (11 sites total).
Previous commit only went to v5 for the three Node-deprecation actions.
Audit of all actions used across the four workflows shows five more
behind by one or two majors:
actions/checkout v5 → v6
actions/setup-node v5 → v6
actions/configure-pages v4 → v6
actions/deploy-pages v4 → v5
actions/upload-pages-artifact v3 → v5
All other pins are already current:
actions/cache v5 (latest)
actions-rust-lang/setup-rust-toolchain v1 (latest)
github/codeql-action/{init,analyze} v4 (latest)
CodeQL's `rust/cleartext-logging` rule (alert #7) taints any value
returned by a function whose name contains "secret" — it can't tell
configuration metadata (the binding identifier from edgezero.toml)
from secret material. The previous rename
`secret_store_name → secret_store_binding` did NOT defeat the
heuristic because "secret" is still in the function name.
Real fix: stop logging the binding name. Operators can read their
own `edgezero.toml` to verify which store binding was configured.
The presence message ("secrets enabled for axum") is still emitted,
which is the only thing the log line was actually load-bearing for.
Updated the affected unit test assertion to match the new wording.
Same heuristic as alert #7 — CodeQL taints any value returned by a
function whose name contains "secret" and tracks it through to HTTP
sinks. The test helper `start_test_server_with_secret_handle` was
flagged because its return value's `base_url` flowed into
`reqwest::Client::get(url)`.
Rename the helper to `start_test_server_with_store_handle` and the
return struct to `TestServerWithStore`. Functionally identical — the
test just bootstraps a dev server with an optional handle. The
remaining `with_secret_handle` builder method on `AxumDevServer` is
unaffected because it returns `Self`, not a sink-bound value.
Three real coverage gaps from earlier commits were untested:
1. `KvStore::put_bytes_with_ttl` overflow error path
(axum/PersistentKvStore). Asserts `Duration::MAX` triggers
`SystemTime::checked_add` overflow and surfaces as
`KvError::Internal("ttl overflows system time")`.
2. `Manifest::try_load_from_str` Err path. Two cases: invalid TOML
bytes and a manifest that fails `validator` (empty config-store
name). Both should return `io::ErrorKind::InvalidData`.
3. `GeneratorError::Format` smoke test. The variant cannot fire in
practice (write-to-String is infallible), but it is part of the
public error surface and the `From<fmt::Error>` wiring must keep
working — assert construction + Display.
Existing coverage for the other behaviour-affecting changes was
already adequate: `KvStore::exists` is exercised by the
`contract_exists` macro across every impl plus 3 dedicated unit
tests, and `Hooks` default-method overrides are exercised by the
`TestHooks`/`DefaultHooks` tests already in app.rs.
ctor 1.0 requires explicit `#[ctor(unsafe)]` to acknowledge that
pre-main static-initialisation runs without the usual Rust safety
guarantees. The annotation is an attribute argument, not an
`unsafe { }` block, so the workspace `unsafe_code = "deny"` lint is
still satisfied. Updated the four adapter cli.rs files
(axum/cloudflare/fastly/spin).
spin-sdk 6.0 is NOT bumped: it raises the MSRV to rustc 1.93 but the
workspace ships rustc 1.91.1 (.tool-versions). Pin stays at 5.2 with
an explanatory comment until we bump the toolchain.
Bumps `.tool-versions`:
rust 1.91.1 → 1.95.0
viceroy 0.16.4 → 0.17.0
Both viceroy 0.17 and spin-sdk 6.0 raised their MSRV to rustc 1.93/1.95
respectively. We can now take viceroy 0.17 freely; spin-sdk 6.0 has
breaking API changes (Method variants → http::Method constants,
`IncomingRequest` removed, Builder::build() → .body()) and is left at
5.2 with a TODO until a focused migration PR.
New 1.95 clippy lints fixed in-place:
- `result_map_unwrap_or_default`: `.map(p).unwrap_or(false)` → `.is_ok_and(p)` (2 sites)
- `manual_map`: `.map(x).unwrap_or(default)` → `.map_or(default, x)` (1 site)
- `duration_suboptimal_units`: `Duration::from_secs(60)` → `from_mins(1)` in
non-const contexts. Two const items keep `from_secs(60 * 60 * 24 * 365)`
with a localized `#[expect(clippy::duration_suboptimal_units, reason =
"from_days/from_mins not stable in const context")]` because
`Duration::from_{mins,days}` const variants are still nightly-only.
- `to_string_in_format_args` / `inefficient_to_string`: replaced two
`ToString::to_string` / `str::to_string` with `str::to_owned`
- `missing_inline_in_public_items`: added `#[inline]` to two proc-macro
entrypoints in edgezero-macros, three EnvOverride methods + the
`env_guard` helper in axum/test_utils, and `From<Action>` for
AdapterAction in cli/adapter.rs
- `doc_paragraph_terminators`: added trailing punctuation to clap doc
comments on every variant/field of `Command`/`NewArgs` (cli/args.rs)
and the `KV_TABLE` doc in axum/key_value_store.rs
Docs:
- CLAUDE.md "Rust": 1.91.1 → 1.95.0
- CLAUDE.md "Fastly CLI": v13.0.0 → 15.1.0
- Fix typo `fasltly` → `fastly` in .tool-versions; remove dup line
- examples/app-demo/.../rust-toolchain.toml: 1.91.1 → 1.95.0
- test.yml: drop the now-stale "1.91 MSRV constraint" comment on the
viceroy install step
Both warnings sat behind `#[cfg]` gates that the `--all-features`
build profile hid:
1. `fastly::init_logger` (no-features stub) needed `#[inline]` —
`missing_inline_in_public_items` only fires when the stub branch
is selected, i.e. when the `fastly` feature is off.
2. `cli::dev_server::EchoParams` (no-`dev-example` build) was
defined after `default_router`/`build_dev_router`; the canonical
item ordering wants structs before fns at module level. Moved
`EchoParams` to the top of the module so the order is correct
in either feature profile.
Surfaces only via `cargo clippy --workspace --all-targets` (no
`--all-features`); the existing CI runs `--all-features` so we did
not catch this until now.
The `https://wasmtime.dev/install.sh` script broke as of 2026-05-19:
its version-detection interpolation failed and it tried to download
literal version `{`, causing the spin-wasm-tests CI job to fail
("Could not download Wasmtime version '{'").
Replace the install path with a direct GitHub-release tarball
download, pinned to the version recorded in `.tool-versions` (same
single-source-of-truth pattern already used for rust + viceroy).
Adds `wasmtime 44.0.1` to `.tool-versions` and a `Resolve Wasmtime
version` step in the workflow that greps it out.
1. `pub_with_shorthand` comment direction was reversed in the
workspace `Cargo.toml`. Confirmed by removing the allow: 6 sites
fire `usage of \`pub\` without \`in\`` (i.e. clippy flags
`pub(crate)` and wants `pub(in crate)`). Restore the allow with
wording that matches the actual lint direction and reflects
the audited 6-site count.
2. Workspace `.cargo/config.toml` was hard-coding the
`wasm32-wasip1` runner to Viceroy, which silently broke
`cargo test -p edgezero-adapter-spin --target wasm32-wasip1`
from the workspace root (used viceroy host ABI instead of
wasmtime).
Fix: remove the workspace-level runner entirely and add a
per-package config for spin (`crates/edgezero-adapter-spin/
.cargo/config.toml`) that selects `wasmtime run`. Fastly
already had its own per-package config. CI continues to
override via `CARGO_TARGET_WASM32_WASIP1_RUNNER` env var, so
workspace-root invocations work in CI without the global
default.
3. Add a module-level doc comment at the top of
`crates/edgezero-adapter-spin/tests/contract.rs` explaining
that the tests cover internal router/dispatch logic, NOT the
Spin host ABI (no `spin_sdk`/WIT imports). A breaking change
in the Spin runtime's WIT would not be caught here.
`parse_handler_path` previously panicked on a syntactically-invalid
handler path in `edgezero.toml`, which rustc surfaced as a confusing
"proc-macro panicked" message. Refactor to return `Result<ExprPath,
String>`; `build_middleware_tokens` and `build_route_tokens` propagate
the error; `expand_app` returns `compile_error!()` with the message,
matching the existing error path for manifest read/parse/validation
failures.
Two new tests: parse_handler_path_accepts_absolute_crate_path (happy
path) and parse_handler_path_rejects_invalid_syntax_with_message
(asserts the error message names the failure and echoes the offending
input).
Addresses the PR review comment on `crates/edgezero-macros/src/app.rs`.
PR reviewer claimed the lint warns *against* longhand and recommends
shorthand (i.e. our `pub(crate)` use should never fire it). Verified
empirically — removing the allow on clippy 1.95 produces 6 errors:
error: usage of `pub` without `in`
| pub(crate) fn decompress_body(...)
| ^^^^^^^^^^ help: add it: `pub(in crate)`
= help: ...index.html#pub_with_shorthand
So `pub_with_shorthand` flags `pub(crate)` and suggests `pub(in crate)`;
the reviewer's reading is 180° off. Quote the diagnostic in the comment
itself so future maintainers don't fall into the same trap.
Sub-project #1 of 7 in the CLI extensions roadmap. Turns edgezero-cli into
lib + bin, exposes per-command Args structs and run_* functions for
downstream projects to compose their own CLIs via clap subcommand
flattening, and adds app-demo-cli as the canonical consumer.
Force-added because docs/superpowers/ is gitignored project-wide for plans;
this spec is shared design intent and meant to be reviewed in the repo.
aram356 added 12 commits June 22, 2026 15:41
CI's wasm32-wasip1 + --features fastly job (no cli) tripped
dead-code warnings on prepare_fastly_config_entries and the
constants only it uses (FASTLY_CONFIG_ENTRY_LIMIT,
CHUNK_PAYLOAD_TARGET, CHUNK_KEY_INFIX, find_utf8_boundary).
That target only needs the runtime resolver path.
Split the module by usage:
- Writer-side (prepare + find_utf8_boundary + the three
writer-only constants) is gated `#[cfg(any(feature = "cli",
test))]` so it compiles for the CLI binary and the in-tree
unit tests but not for the runtime-only wasm build.
- Resolver-side (resolve_fastly_config_value + sha256_hex +
POINTER_KIND + the pointer schema) stays unconditional --
config_store.rs calls it under `#[cfg(feature = "fastly")]`.
Reviewer flagged four issues:
- Bash 4+ `${adapter^^}` and `${!skip_var}` failed on macOS's
/usr/bin/env bash (3.2) with "bad substitution"; the script
silently false-passed. Replaced with a portable `upper()` helper
(tr-based) + `eval "skip_val=\${${skip_var}:-0}"` indirect.
- Axum row hardcoded PORT=8765 while app-demo's edgezero.toml binds
to 8787, so the wait_for_port loop polled the wrong port and never
saw the staging blob. Aligned PORT=8787 across all four rows.
- Fastly/Spin boot helpers ran viceroy / spin against
target/...debug/*.wasm artifacts that a clean checkout doesn't
have. Added an ensure_runtime_built() step that cargo-builds the
required wasm target before the boot.
- §12.7 fastly row and §9.3 oversized smoke both rewrote
examples/app-demo/crates/app-demo-adapter-fastly/fastly.toml in
the checked-in tree. Added backup_in_tree() + cleanup-time
restore so the smoke leaves the worktree clean. The other local-
state directories (.wrangler/, .spin/, .edgezero/) are gitignored
and need no backup.
Also reviewer-flagged: migration guide's Spin SQLite cleanup
example preserved only `app_config`, which would delete operator-
configured staging/canary blobs. Updated to mention every
`__KEY`-selectable key.
…ault
Two reviewer-flagged regressions in the §12.7 / §9.3 smoke:
- The curls were targeting /config/greeting (the legacy raw
config-store handler at app-demo edgezero.toml:57), which
always 404s on the blob model. The Phase E requirement is
to prove the AppConfig<AppDemoConfig> extractor reads the
right blob -- that's /config/typed (handlers.rs:185).
Repointed both the per-adapter __KEY override assertions
and the Fastly oversized chunk-pointer assertion.
- cleanup() did BOTH stop_server() + restore_backups(); the
per-row teardown called it between the staging assertion
and the default-blob reboot, so fastly.toml got restored
before step 4 could read the default blob. Split into
stop_server() (kill only) and restore_backups() (restore
only); step 3 calls stop_server() to keep pushed state
intact, step 4's end-of-row uses the combined cleanup().
Verified locally on Bash 3.2: all-skip mode prints SKIPPED
rows cleanly with 0 passed, 0 failed (no "bad substitution").
Three reviewer-flagged correctness issues + a comment-style sweep:
- config push --key was wiping sibling keys. Both Axum (cli.rs:236)
and Fastly local (cli.rs:895) rewrote the whole config map on
every push; pushing default then staging left only staging.
Fixed both to upsert per-key (spec 12.7 requires default +
staging to coexist for the runtime KEY override). Added
push_config_entries_preserves_sibling_keys for Axum and
push_config_entries_local_preserves_sibling_keys for Fastly.
- Smoke harness now seeds demo_api_token for the Axum row so the
AppConfig<AppDemoConfig> extractor's secret walk resolves before
the assertion fires; the stop_server helper SIGTERMs, waits up to
5s, then SIGKILLs survivors and polls until the port is free so
the next boot's wait_for_port doesn't race the previous socket
close.
- Three new trybuild compile-fail fixtures pin the non-secret
coverage spec 4.2/12.1 calls for: skip_serializing, skip_serializing_if,
and flatten are banned on EVERY field, not just #[secret] ones.
The macro already enforced this universally; the fixtures lock
it in.
Plus a comment-style sweep across 36 source files:
- Strip plan-process tags (Step N, Task <X>N, Phase N, round-N,
C4 Step 5 etc) from comments; spec-section refs stay.
- Strip the section sign (U+00A7) from comment + error-message
strings. Trybuild .stderr fixtures regenerated to match.
- second_oversized_push_converges_runtime_on_new_envelope gets
#[expect(clippy::too_many_lines)] -- splitting the linear push
A -> inspect -> push B -> inspect -> read scenario obscures the
chunk-set comparison.
All five workspace gates clean + all three Phase C CI gates clean +
docs prettier clean.
Two reviewer-flagged smoke-harness issues:
- stop_server waited for the port to free but didn't fail when it
stayed live. On the prior run the first server kept serving
staging into the default-blob assertion. stop_server now:
* pkill -TERM the SERVER_PID + children, wait 5s
* pkill -KILL stragglers
* lsof -ti :PORT and kill -9 any process still bound
(catches grand-children that pkill -P misses)
* verify port is free within 10s; if not, log FAIL and
return non-zero.
Per-row callers now check stop_server's exit and skip to
cleanup on failure (no silent default-key assertion against
a stale runtime). boot_runtime also refuses to launch if the
port is already bound -- prevents wait_for_port from racing
the previous server's response.
- Only Axum was secret-seeded (env var). Cloudflare/Fastly/Spin
required manual platform setup, so the all-adapter smoke
failed in the secret walk before reaching the KEY override.
New seed_secret_for_adapter helper writes per-adapter local
secret state:
* cloudflare: .dev.vars file with demo_api_token=resolved-token
(gitignored). New per-row backup_in_tree call so cleanup
restores the worktree.
* fastly: appends [local_server.secret_stores.default] block
to fastly.toml (caller's backup_in_tree already covers
this).
* spin: SPIN_VARIABLE_DEMO_API_TOKEN=resolved-token at boot.
Fastly chunk-pointer smoke now seeds the same secret before
the runtime read.
Three reviewer-flagged smoke issues on 699bdc9:
- The fastly.toml fixture's [local_server.secret_stores.default]
is an array-of-tables (each entry exposes one key + the env
var to read its value from). The prior seed appended a
normal-table block at the same path; TOML rejects the mix.
Switched to a [[local_server.secret_stores.default]] append
with key = "demo_api_token" + env = "DEMO_API_TOKEN_SECRET".
Viceroy boots with DEMO_API_TOKEN_SECRET=resolved-token. The
whole edit is covered by the existing fastly.toml
backup_in_tree.
- The Spin fixture's spin.toml declared `api_token` (the
AppDemoConfig field name) but the runtime secret walk asks
for the blob VALUE -- "demo_api_token". An awk patch now
appends `demo_api_token = { required = true, secret = true }`
to the [variables] block AND the matching
demo_api_token = "{{ demo_api_token }}" line to the
[component.app-demo.variables] map. spin.toml is added to
the per-row backup_in_tree so cleanup restores it.
- The lsof port-cleanup log line used single quotes around
'${PORT}', printing the literal text. Replaced with a
printf %s + "$PORT" so the actual port number renders.
Verified both patched TOMLs round-trip through tomllib.
Three reviewer-flagged issues:
- examples/app-demo/crates/app-demo-cli config_flow.rs imports
edgezero_core::secret_store::InMemorySecretStore, which is
gated behind the test-utils feature. CI runs
`cd examples/app-demo && cargo test --workspace --all-targets`
and was blocked at compile. Enabled
features = ["test-utils"] on the edgezero-core dev-dep.
- examples/app-demo/Cargo.lock was stale after the Fastly
chunked-config commit added serde + sha2 as non-optional
deps to edgezero-adapter-fastly. Regenerated; --locked
workspace test now passes.
- smoke_test_config_key_override.sh false-greened when an
unskipped runtime row failed to boot. boot_runtime
propagated the error, but the caller's `cleanup; continue`
loop incremented neither PASS nor FAIL, so a sandbox bind
failure printed `Results: 0 passed, 0 failed` and exited 0.
Every unskipped row's boot/seed failure now bumps FAIL with
an explicit `FAIL <adapter> row: ...` log line. stop_server
already accounted via its own port-still-live diagnostic;
the per-row site just notes that.
Reviewer ran SKIP_SPIN_CLOUD_SMOKE=1 ./scripts/smoke_test_config_key_override.sh
on c64558a from a dev machine with prior .wrangler/state in
place. The Axum row passed but Cloudflare local row failed at:
remote envelope parse failed: expected value at line 1 column 1
The CLI is correct to reject it: the cutover spec hard-fails
when read-back finds a non-BlobEnvelope value at the target
key. The smoke harness was wrong to leave gitignored emulator
state in place; the new push reads the existing value (per
read-back skip-on-equal + diff) and the stale data trips the
parse.
Per-row reset of the local-state directory before the push:
- axum: rm -rf examples/app-demo/.edgezero
- cloudflare: rm -rf .../.wrangler
- spin: rm -rf .../.spin
- fastly: no-op (fastly.toml IS the local store, already
backup_in_tree'd)
All four directories are gitignored and regenerated by the push.
Worktree stays clean (backup_in_tree of fastly.toml/.dev.vars/
spin.toml restores the tracked-fixture mutations on cleanup).
Wrangler 4.x (verified 4.64.0) returns exit 0 + stdout
"Value not found" for a missing key instead of exit 1 + stderr.
The previous read path treated every exit-0 stdout as a
Present envelope, which made the next CLI step try to parse
"Value not found" as a BlobEnvelope and abort with:
remote envelope parse failed: expected value at line 1
column 1
A missing key in the blob model is valid initial state (the
first push hasn't run yet), not corrupt remote state, so it
must map to ReadConfigEntry::MissingKey.
Detect by trimming the success-branch stdout and matching
'value not found' / 'value not found.' case-insensitively
before returning Present. Adds a fake-Wrangler regression
test that pins the exit-0 stdout shape verbatim.
Also: stale Fastly comment said entries arrive pre-flattened
(per-leaf model). Reworded to describe the blob-envelope
shape and the chunked_config expansion path.
- Cloudflare: env_config_from_worker only queried __NAME for
each store id; the runtime never saw __KEY, so a staging
push at app_config_staging silently fell back to the
default blob. Spec 5.4 routes the runtime extractor via
EDGEZERO__STORES__CONFIG__<ID>__KEY. Added a __KEY query
for CONFIG ids alongside the existing __NAME (KV / SECRETS
bindings have no per-id key override and are unchanged).
Smoke boot for Cloudflare now writes both the secret and
the per-row __KEY env override into .dev.vars so
`wrangler dev` surfaces them to env.var(...).
- Spin: the fixture declared `api_token = { required = true,
secret = true }`, but the blob's secret-key NAME is
`demo_api_token`. spin up failed before serving because
the required `api_token` had no provider. Smoke awk patch
now ALSO downgrades the legacy `api_token` line to
`{ default = "", secret = true }` while inserting the new
`demo_api_token` declaration; the obsolete field can
remain unset without blocking startup.
- Fastly: viceroy 0.17.0 uses `serve` for the long-running
HTTP path (`run` was renamed). Smoke boot updated.
- Cloudflare cli.rs: dropped "pre-flattened/dotted form"
wording in two doc comments; the writer now handles one
logical blob-envelope (key, envelope_json) per push.
- Fastly smoke booted from the wrong wasm path: cargo writes
examples/app-demo/target/wasm32-wasip1/debug/app-demo-adapter-fastly.wasm
(workspace target + hyphenated name), but the smoke ran from
the adapter crate dir and reached for
target/wasm32-wasip1/debug/app_demo_adapter_fastly.wasm.
Smoke now serves the absolute workspace-target path with the
correct hyphenated filename.
- Tracked Spin fixture had the wrong variable name. The
AppDemoConfig field `api_token` HOLDS the secret-store key
NAME (`demo_api_token`, per Model A); the runtime secret
walk asks Spin for `demo_api_token`, not `api_token`. The
fixture declared a required `api_token` variable that
blocked `spin up` before the row could even fail meaning-
fully. Renamed the [variables] entry and the matching
[component.app-demo.variables] mapping to `demo_api_token`
with default = "" so `spin up` starts without a configured
provider. Smoke drops its awk patch and spin.toml backup-
restore (no longer mutating the fixture).
- spin-sdk ~6 imports wasi:http/types@0.3.0-rc-2026-03-15
which Spin < 3.7 doesn't provide. Smoke's spin row now
pre-checks `spin --version` and SKIPs with a clear note
if CLI is missing or older than 3.7. Documented the
requirement in .tool-versions (asdf has no widespread
spin plugin so the version isn't pinned there).
Fastly Compute@Edge has no process env -- `EnvConfig::from_env()`
reads `std::env::vars()`, which returns empty inside the wasm
guest. That meant
EDGEZERO__STORES__CONFIG__APP_CONFIG__KEY=app_config_staging
was silently dropped by the runtime, and the spec 12.7 staging
override fell back to the binding's default id.
Mirror what the Cloudflare adapter does: open a dedicated
`edgezero_runtime_env` Config Store, probe for the known
EDGEZERO__* keys derived from `StoresMetadata`, and feed
those into `EnvConfig::from_vars`. Missing-store is a no-op
(empty env, same as the pre-fix default behaviour).
Smoke now seeds the local viceroy copy of that store with the
per-row `__KEY` value via a `seed_fastly_runtime_env` python
helper that idempotently rewrites the
`[local_server.config_stores.edgezero_runtime_env]` block in
fastly.toml (already covered by the existing backup_in_tree
cleanup).
Operators deploying to remote Fastly create the matching
Config Store with `fastly config-store create --name=edgezero_runtime_env`
and `config-store-entry update --upsert` per env-var key.

@ChristianPavilonisChristianPavilonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ts serve command could use better ergonomics.

currently a user must run ts config push --adapter fastly --local

it would be better if the app config was automatically pushed to the local setup either by default or with a command flag

Reviewer flagged that the runtime store the previous commit
introduced was never provisioned and the docs still told
operators to set EDGEZERO__* as a shell env var.
- `edgezero provision --adapter fastly` now creates the
`edgezero_runtime_env` Fastly Config Store alongside the
declared app stores. Skips if the
`[setup.config_stores.edgezero_runtime_env]` block is
already present in fastly.toml. Output line nudges the
operator with the exact `config-store-entry update --upsert`
invocation for the staging key.
- The runtime path in env_config_from_runtime_dictionary
now logs a one-shot warning when the store is missing
(instead of silently falling back to baked defaults), so
operators see the gap in Fastly logs and can run provision.
- README.md.hbs scaffold mentions the per-adapter override
mechanism explicitly: Axum=process env, Cloudflare=worker
vars, Spin=application variables, Fastly=Config Store
created by provision.
- Migration guide gets a per-adapter table for where to set
__KEY, plus a Fastly-specific block walking through the
config-store-entry update flow + the runtime warning
behaviour when the store isn't provisioned.
Three provision unit tests updated for the new line / skip
fixture; provision_dry_run_does_not_invoke_fastly expects
4 lines (now includes the runtime-env row);
provision_with_no_declared_stores_says_so and
provision_skips_id_when_setup_block_already_present
pre-populate `[setup.config_stores.edgezero_runtime_env]`
so the new step skips and the tests don't shell out to
real `fastly`.
The declared-store provisioning path emits a remediation note
when fastly.toml carries `service_id` -- the next `compute
deploy` won't re-apply `[setup]`, so a freshly-created store
needs a `fastly resource-link create` to attach to the live
service. The runtime-env branch I added in the previous commit
created/appended the setup block but skipped that same check,
leaving operators with a runtime warning ("`edgezero_runtime_env`
not found") even after running provision against an
already-deployed service.
Factored the post-create note into a `resource_link_note`
helper and called it from both the declared-store loop and
the runtime-env branch. The runtime-env line now carries
BOTH the populate-keys hint AND (when service_id is set) the
resource-link command.
Two regression tests on the helper directly:
- provision_emits_resource_link_note_for_runtime_env_on_existing_service:
service_id set => note quotes the id, the store-id lookup
command, the resource-link command with `--name=edgezero_runtime_env`.
- provision_skips_resource_link_note_when_service_undeployed:
no service_id => returns None so the next `compute deploy`'s
`[setup]` pass handles it without a stale-prompt false positive.
@aram356aram356 mentioned this pull request Jun 26, 2026
14 tasks
Spec at docs/superpowers/specs/2026-06-23-provision-local.md and
plan at docs/superpowers/plans/2026-06-27-provision-local.md after
many rounds of external review. Covers:
- --local CLI/mode split + dispatch matrix (cloud vs local x dry-run)
- Path containment helper rejecting absolute / .. / out-of-crate paths
- First-run bootstrap synthesis (CLI-owned, before validate, local-only)
- Dry-run staging via tempfile::TempDir (real copy for mutable paths)
- ManifestAdapterDeployed schema + Manifest-level cross-adapter validator
- Cloudflare/Fastly/Spin/Axum local writers (logical vs platform split)
- Cloudflare/Fastly/Spin manifests gitignored; axum.toml stays tracked
- Generated <app-cli>'s run_provision_typed for #[secret] placeholders
- Adapter-scoped run_serve env-file load (axum vs spin)
- Smoke warm-up via app-demo-cli provision --adapter <name> --local
- Per-adapter contract tests + Spin env-label alignment
Folds in many rounds of external review against the spec and plan
without changing the v1 contract:
- run_with_staging takes a project-relative crate path instead of
an absolute one; removes strip_prefix bug for default
`--manifest edgezero.toml` (project_root = ".").
- Adapter::provision gains `deployed: Option<&AdapterDeployedState>`
threaded through from the CLI boundary translator -- adapters
never re-parse edgezero.toml.
- Synthesis stays CLI-owned (Task 8b); adapter local arms assume
the baseline is on disk and error if it isn't.
- Fastly local arm upserts top-level service_id from deployed,
not just on first synthesis.
- Fastly contents = empty [..contents] sub-table (NOT contents = "")
to match the existing push writer's table-or-error expectation.
- Synthesisers use toml_edit::DocumentMut (not raw format!) so
pathological [app].name values escape correctly.
- Spin provision_typed resolves component_id from spin.toml's
[component.*] (manifest, not app_name; provision_typed doesn't
receive app_name).
- ValidationContext gains pub(crate) accessors so run_provision_typed
can read manifest()/app_name()/app_config_path() without
field-private compile errors.
- run_typed_preflight takes &ValidationContext directly (no
TypedPreflightInputs wrapper -- Manifest doesn't derive Clone).
- Dry-run report uses resolved adapter_manifest_abs, not static
filenames; handles manifests in subdirs of the adapter crate.
- CI gate + downstream untrack regex + Task 33 local sanity all
cover .dev.vars (Cloudflare secret placeholder).
- Spec base-vs-typed split applied to Axum/Cloudflare/Spin
per-adapter local-state rows.
- Path safety helper short-circuits Step 1 starts_with when root
is "." (relative-root resolution).
- Adapter manifests are gitignored; .dev.vars too. axum.toml stays
tracked.
Task 10's run_with_staging now takes the project-relative crate
path directly, so the resolve_adapter_crate_dir helper was
removed from Task 11. Three small wording cleanups so the plan
text matches the actual shape:
- Task 10 implementation snippet: rename param adapter_crate_dir
to adapter_crate_rel (signature matches body + callers).
- Task 11 intro: drop resolve_adapter_crate_dir from the
"placeholder helpers this task resolves" list.
- Task 29 helper-list bullet: drop the stale entry.
- Task 34 (run_serve) docstring: rephrase to stop pointing at
the removed helper; the comment now justifies why run_serve
keeps its own resolver.
No behavioural changes.
@aram356
aram356 merged commit e483723 into mainJun 29, 2026
13 checks passed
@aram356
aram356 deleted the feature/extensible-cli branch June 29, 2026 21:20
aram356 added a commit that referenced this pull request Jul 2, 2026
- Define config-store / kv-store / secret-store capability semantics
(bare enum variants had no doc comment, unlike the six outbound caps).
- Add explicit transport-error mapping bullets to the Cloudflare (§4.2) and
Spin (§4.4) adapters, matching Axum/Fastly (DNS/TLS/connect -> bad_gateway).
- Collapse the now-obsolete dual-baseline enforcement-gate hedging: PR #269
has merged to main, so the #269 sibling-gate topology is the active shape and
the pre-#269 wording is marked historical.
aram356 added a commit that referenced this pull request Jul 2, 2026
- §4.3: document Fastly send_all cancellation/drop semantics (no async
cancellation; every PendingRequest is harvested; sibling deadline never
aborts other slots).
- §3.3.1 / §3.1.3: spell out Deadline::after overflow fallback and
into_response()'s only Err (adapter-invariant internal) condition.
- §5.4: add test rows for Json/ValidatedJson cap migration, the *Within
explicit-cap extractors, the per-adapter capability() matrix, and
back-compat manifest parse.
- §5.5: state where Tier 2 runs per adapter (host gate for Axum; per-adapter
wasm-target matrix for the three WASM adapters).
- §7: add dispositions for adapter request.rs, the Tier 3 MockServer, the
run_* gate-site files + ensure_capabilities home, and CLAUDE.md/workflow
wasip2 refresh; note sha2 is already a workspace dep.
- Collapse the now-obsolete dual-baseline (#269 pre/post) framing in §3.5.2,
§5.5, and §6 to the post-#269 shape, keeping the pre-#269 note as history.
aram356 added a commit that referenced this pull request Jul 18, 2026
Removes internal spec-section references (§5.4, 8.x, 9.x, 12.x), PR/round/finding
markers (PR #269, Post-F4/F6, F1/F2), plan-phase labels (Stage 5/6), and
test-doc severity prefixes from comments and user-facing strings in the files
this branch touches. The explanatory content is preserved verbatim — only the
process reference is dropped and the surrounding sentence re-flowed. Comments now
describe what the code does, not which spec section or review round produced it.
Two deliberate survivors: the raw-push error string points at the real design
doc's §3.2.1 (a user-facing pointer, and asserted by tests), so it and its test
assertions are left intact.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@aram356@ChristianPavilonis@prk-Jr