Skip to content

Implemented axum adapter - #4

Merged
aram356 merged 3 commits into
mainfrom
feature/axum-adapter
Oct 3, 2025
Merged

Implemented axum adapter#4
aram356 merged 3 commits into
mainfrom
feature/axum-adapter

Conversation

@aram356

Copy link
Copy Markdown
Contributor

No description provided.

@aram356
aram356 merged commit 6292cd8 into mainOct 3, 2025
2 checks passed
@aram356
aram356 deleted the feature/axum-adapter branch October 3, 2025 19:52
aram356 added a commit that referenced this pull request Jun 29, 2026
…rovision/config (#269)
* Remove arbitrary_source_item_ordering allow; reorder ~300 sites
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.
* Remove as_conversions workspace allow; eliminate 8 cast sites
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.
* Remove arithmetic_side_effects allow; use checked/saturating ops
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.
* Pin Viceroy to ^0.16 in CI
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.
* Pin viceroy 0.16.4 in .tool-versions
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.
* Read Viceroy version from .tool-versions in CI
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.
* Remove min_ident_chars allow; rename ~190 single-char identifiers
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.
* Document why module_name_repetitions stays as workspace allow
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.
* Document why module_name_repetitions stays as workspace allow
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.
* Remove stray libtest_lint.rlib build artifact, ignore *.rlib
* Remove float_arithmetic allow; use integer ms in request logger
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.
* Document why exhaustive_enums stays as workspace allow
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.
* Remove missing_inline_in_public_items allow; add #[inline] to ~321 fns
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.
* Rename Manifest::secret_store_name → secret_store_binding
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`.
* Bump checkout/setup-node/cache actions v4 → v5 (Node 24 runtime)
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).
* Bump remaining CI actions to current latest majors
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)
* Upgrade redb 4.0 → 4.1
* Fix CodeQL rust/cleartext-logging by dropping binding name from log
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.
* Fix CodeQL rust/cleartext-transmission (#9, #10): rename test helper
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.
* Add tests for behaviour added in this PR
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.
* Upgrade ctor 0.10 → 1.0; document spin-sdk 6.0 MSRV gap
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.
* Upgrade toolchain to rust 1.95.0; bump viceroy to 0.17
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
* Fix two clippy warnings only visible on no-features build
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.
* Pull edgezero_adapter_axum::dev_server::run_app via use in app-demo
* Fix spin CI: pin wasmtime via .tool-versions + direct GitHub tarball
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.
* Address PR review comments
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.
* Surface invalid handler paths via compile_error! instead of panicking
`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`.
* Document pub_with_shorthand with verbatim clippy diagnostic
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.
* Add design spec for extensible edgezero-cli library
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.
* Expand CLI extensions spec to cover all 7 sub-projects
Replaces the sub-project-#1-only spec with a single design document that
covers the full effort: extensible edgezero-cli library, generator updates
for <name>-cli and <name>.toml scaffolding, per-service typed app-config
schema with validator integration, four new commands (auth, provision,
config validate, config push), shell-out mocking via a private
CommandRunner trait, and the app-demo overhaul that exercises everything
end-to-end.
Implementation still ships in 7 incremental PRs but the design decisions
live in one place so reviewers see the whole picture.
Force-added because docs/superpowers/ is gitignored project-wide.
* Apply review feedback and add secret annotation to CLI extensions spec
High-severity fixes:
- Add --manifest to ProvisionArgs and ConfigPushArgs (matches validate)
- Update Wrangler invocations to 3.60+ syntax (space-form, --namespace-id)
- Persist provisioned IDs in edgezero.toml [stores.*.adapters.<x>].id;
cross-write to per-adapter manifests where deploys need them
- Mermaid diagram in §3 replacing ASCII art
Medium-severity fixes:
- config push runs strict validation as pre-flight (no separate flag)
- Move --adapter to each AuthSub variant so UX is `auth login --adapter X`
- Constrain typed config push to serde_json::to_value(C) -> Object;
document flatten / rename / skip / Option::None handling
- Unify raw + typed serialization rules; raw drops Validate + secret skip
- Replace CommandRunner positional args with CommandSpec struct
(program, args, cwd, stdin, env)
- "Backwards-compatible" language replacing "unchanged" for default bin
- Move walkthrough doc to docs/guide/ with explicit sidebar update
Low + open questions:
- Document consumer-facing Cargo feature names and adapter opt-outs
- Generator migration note: sub-project 1 outputs don't auto-migrate
- Deprecate [stores.config.defaults] in favor of <name>.toml [config]
- Mark Spin provision / config push as "not yet supported" with pointer
to the in-flight Spin stores PR; clear error message until then
Secret annotation:
- New §6.6 documenting #[derive(AppConfig)] from edgezero-macros
- #[secret] field attribute marks runtime-secret-store-backed fields
- Toml value for those fields is the secret-store binding name
- config validate (typed) cross-checks the binding appears in [stores.secrets]
- config push (typed) skips SECRET_FIELDS entirely
The implementation still ships in 7 incremental PRs.
* Expand spec for multi-store manifest + finalize naming and validate scope
Manifest schema rewrite (new sub-projects #2 and #3):
- [stores.<kind>].ids = [...] + default declare the logical stores the
app uses (kv, secrets, config all multi-store)
- [adapters.<X>.stores.<kind>.<id>].name = "..." maps each logical id to
the platform-specific name on adapter X, with optional adapter-specific
tuning fields stored as free-form extras
- Provisioned platform resource IDs (Cloudflare namespace ID, Fastly
store ID) live in each platform's native manifest (wrangler.toml,
fastly.toml), not in edgezero.toml. provision writes them there;
config push reads them back.
- RequestContext store accessors become id-keyed:
ctx.kv_store("id") / ctx.kv_store_default() (and similarly for
config_store / secret_store). Each adapter builds a StoreRegistry<H>
at request setup from [adapters.<self>.stores.*].
- Manifest validator enforces: ids non-empty; default in ids;
every adapter has a name mapping for every id.
Naming:
- Field on the per-adapter block is `name` (matches the user's example),
not `binding`. The Cloudflare wrangler.toml term `binding` is now
called out as wrangler's terminology, not ours.
Secret references (§6.7):
- The string a #[secret] field holds is an app-defined reference; the
spec documents both valid runtime patterns (logical store id or key
within the default secret store). Validate just confirms the string
is non-empty and that the app has a secret store available.
config validate (§11) explicitly covers app-config validation:
- TOML syntax, [config] table presence, type matching against C,
serde-rejected unknown fields, validator business rules, non-empty
secret references, and the manifest-side cross-checks.
Sub-project count: 7 → 9 (added schema rewrite + RequestContext API
rewrite as #2 and #3; existing app-config/validate/auth/provision/push/
polish become #4-#9).
This is a breaking change to the on-disk manifest schema; the in-tree
example/app-demo is migrated as part of the work, and a migration guide
ships with sub-project #2.
* Apply second-pass review: runtime API completeness, Cloudflare KV, secret forms
HIGH severity fixes:
- Cloudflare config store rewritten from [vars] to KV (§6.9) so
`config push` actually reaches the runtime without redeploying.
Lands in sub-project #3 alongside the rest of the runtime work.
- Sub-project #2 is now purely additive on the schema: no runtime
changes, no removal of [stores.config.defaults]. The runtime bridge
and the defaults removal move out of #2 (into #3 and #9 respectively).
- Spin completeness: validator skips adapters without an
[adapters.<X>.stores] section. App-demo's Spin adapter omits stores
until the in-flight Spin stores PR lands.
- Extractor design (§6.8): existing Kv / Secrets extractors keep
working as default-store accessors; new KvNamed<const ID> /
SecretsNamed<const ID> extractors give type-safe named access. No
handler-facing break.
- Hooks, ConfigStoreMetadata, and app! macro added to sub-project #3
scope; they all become id-keyed. Multi-store rewrite is now complete.
MEDIUM severity fixes:
- Validate bound is DeserializeOwned + Validate + AppConfigMeta (no
Serialize). The serde_json::to_value object check is push-only;
push adds Serialize.
- Secret semantics: two explicit forms via attribute. #[secret] = key
inside the default secret store. #[secret(store_ref)] = logical store
id in [stores.secrets].ids. Validate cross-checks the latter.
- AppConfigMeta::SECRET_FIELDS is now &'static [SecretField] carrying
SecretKind so the CLI can apply the right validation per field.
- #[secret] constrained to non-flattened, non-renamed scalar fields;
combinations with #[serde(flatten)] / rename / skip produce compile
errors. Macro tests cover the constraints.
- Unknown-field rejection is no longer a validate guarantee; the
generator template emits #[serde(deny_unknown_fields)] on the
generated config struct so new projects opt in by default.
- Every public *Args derives Default + #[non_exhaustive]; external
construction documented as Default + field mutation.
LOW severity fixes:
- Macro example fixed: #[proc_macro_derive(AppConfig, attributes(
secret))] in edgezero-macros/src/lib.rs directly. No bogus _impl
re-export.
- Cloudflare-invalid JS-identifier `name` values are errors (would
break worker deploy), not warnings.
Sub-project ordering and risk:
- #2 risk dropped to L (purely additive).
- #3 grows to absorb Cloudflare KV swap + Hooks/macro/extractor.
- #9 now also drops [stores.config.defaults] and wires axum dev-server
to seed from <name>.toml.
* Third-pass review: async ConfigStore, env overlay, extractor refactor
HIGH severity fixes:
- ConfigStore::get becomes async (#[async_trait(?Send)]). Cloudflare
config moves [vars] -> KV with real async reads. Cascade (trait, 3
adapter impls, Hooks, handlers, extractors) contained to #3.
- Drop const-generic &'static str extractors (don't compile on stable
1.95). Kv / Secrets extractors refactored to yield a registry handle
with default() / named(id) accessors.
- Introduce BoundKvStore / BoundConfigStore / BoundSecretStore so
runtime accessors return a handle bound to the resolved platform
name; callers just .get(key).await.
- Sub-project #2 models logical store declarations as
Option<LogicalStoreConfig> so old-shape manifests (None) are
distinguishable from new-but-incomplete ones (Some with empty ids).
Keeps #2 genuinely additive.
MEDIUM severity fixes:
- Fastly native-manifest writeback: spec commits to a read/write-path-
agreement contract; exact fastly.toml sections pinned in #7's plan.
- Adapter store completeness uses an explicit
STORES_SUPPORTED_ADAPTERS allowlist (axum, cloudflare, fastly). A
supported adapter omitting [adapters.<X>.stores] is an error; only
non-allowlisted adapters (spin) skip.
- All "default store" prose uses the resolved default id (explicit
default, else single ids[0]).
- AuthArgs no longer derives Default (avoids a placeholder subcommand
leaking into a real auth path). §6.11 documents which *Args get
Default.
- config push gains explicit "validate passes, push serialization
fails" test scenarios (non-object typed config, compound shapes,
skip_serializing_if, Option::None, flatten).
LOW severity:
- Ship-gate wording: existing commands stay backwards-compatible
rather than "edgezero --help unchanged" (false once auth/provision/
config land).
New requirement - environment-variable override resolution (§6.10):
- load_app_config overlays env vars on the toml [config] table.
- Env var format: <APP_NAME>__<SECTION>__..__<KEY>; __ separates every
nesting level; APP_NAME is [app].name uppercased, hyphens to
underscores.
- Type coercion against the target TOML type; --no-env escape hatch on
validate and push.
app-demo (§15) now explicitly exercises every new capability: multi-
store, async config, named-kv extractor, nested config section, env
override, both secret forms, validate/push, auth/provision via mock.
* Fourth-pass review: manifest discrimination, Hooks split, env coercion, Fastly contract
HIGH severity fixes:
- Manifest old-vs-new discrimination corrected. Existing manifests
already have [stores.kv/secrets/config] tables, so table-presence
can't discriminate. Sub-project #2 now uses compatibility structs
carrying legacy fields (name, legacy adapters) plus new logical
fields (ids, default) side by side; the discriminator is
ids.is_some(). The current app-demo edgezero.toml parses unchanged.
- Hooks cannot return bound handles. Hooks / ConfigStoreMetadata are
static compile-time app metadata; bound handles need per-request
adapter state. Split: Hooks/app! emit store metadata registries;
only RequestContext returns Bound*Store handles. Adapters consume
the metadata at request setup to build the runtime registries.
- Env overlay type coercion: with C: DeserializeOwned there is no
pre-deserialization type reflection. Env vars now override existing
keys only, coerced to the existing TOML value's type. Matches the
current AxumConfigStore::from_env behavior. To make a key
env-overridable it must appear in <name>.toml.
- Axum config push and runtime read agreed: the axum config store is
backed by .edgezero/local-config-<id>.json; config push --adapter
axum writes that file; edgezero dev regenerates it at startup. No
more disagreement between push target and dev-server source.
MEDIUM severity fixes:
- Fastly writeback contract made concrete from Fastly's docs:
[setup.<kind>_stores.<name>] + [local_server.<kind>_stores.<name>]
keyed by resource link name (== our `name`). provision creates the
store and ensures both fastly.toml sections exist; config push
resolves the store id on demand via `fastly config-store list
--json` (Fastly has no stable persisted id slot). Read/write paths
all key off [adapters.fastly.stores.<kind>.<id>].name.
- Env key matching is deterministic and ambiguity-rejecting: keys
transform to an env segment form (uppercase); two siblings mapping
to the same segment is an AppConfigError. No case-insensitive fuzzy
fallback.
- Cloudflare KV eventual consistency: §6.9 no longer claims values are
live "on the next request"; CI does not assert immediate global
Cloudflare visibility.
LOW severity:
- BoundSecretStore keeps the existing bytes::Bytes API (get ->
Option<Bytes>, require_str), not Vec<u8>.
* Fifth-pass review: hard cutoff, Spin as first-class store adapter, one-PR delivery
Hard cutoff (per user directive — projects fully migrated, no compat):
- Removed all old-vs-new manifest discrimination: no compat structs,
no ids.is_some() check, no legacy-field parsing. The store schema is
rewritten outright. Legacy fields (name, legacy adapters overrides,
[stores.config.defaults]) are hard load errors pointing at the
migration guide.
Spin as a first-class store-capable adapter (PR #253 baseline):
- Removed the "Spin deferred" non-goal. Spin participates fully.
- New §6.7 Spin store semantics: KV is label-backed multi-store with a
max_list_keys cap; config and secrets are both spin_sdk::variables —
a single flat namespace, lowercase [a-z0-9_] keys, no dots.
- Replaced the flat STORES_SUPPORTED_ADAPTERS allowlist with an
adapter x kind capability matrix (Multi vs Single). Validation: if
any target adapter is Single for a kind, [stores.<kind>].ids must
have exactly one id (you cannot have two config stores if you also
target Spin).
- §6.4 config key model: nested config flattens to dotted keys;
canonical handler form is dotted; Spin config store translates
. -> __ internally; config push writes platform-native key form.
- Spin wired into commit 2 (runtime registry, async ConfigStore now
cascades across all FOUR adapters), commit 6 (provision: spin.toml
writeback for key_value_stores / [variables] /
[component.<name>.variables]), commit 7 (config push: Spin variables
in spin.toml).
- provision now has explicit axum (no-op, prints local-store note) and
spin (manifest writeback, no CommandRunner) contracts; config push
is split per adapter — no universal native-resource-ID assumption.
Other review fixes:
- Default resolution made strict: `default` required when ids.len() > 1.
- Docs config path corrected to docs/.vitepress/config.mts (not .ts).
Delivery: one PR with eight commits (one per sub-project), not eight
PRs. CI gates the PR head; each commit should still build for
bisectability. Sub-project count stays at 8 (manifest+runtime stay
merged as the atomic commit 2).
* Sixth-pass review: close Spin integration design holes
Nine findings against the current (f0aed20) spec, all Spin-integration
depth:
- Spin provision cannot know config/secret variable keys (manifest has
store ids, not field keys). Fix: Spin provision does KV-label
spin.toml writeback ONLY. Config-variable declaration moves to
config push (which loads <name>.toml). Secret-variable declaration
is manual.
- config push --adapter spin must write BOTH [variables] (declaration
+ default) and [component.<name>.variables] (binding) — a Spin
variable is unreadable without the component binding. Errors rather
than writing a half-configured manifest.
- Spin component discovery specified: parse spin.toml; single component
resolves implicitly; multi-component requires
[adapters.spin.adapter].component; config validate --strict surfaces
failures early.
- Secret variables are not inferable (#[secret(store_ref)] runtime keys
are code-local). Spin secret variables are declared manually by the
developer; the CLI never writes them.
- Config/secret namespace collision guarantee was wrong: #[secret]
field VALUES (not Rust field names) are the secret keys. config
validate now computes the effective Spin variable set
({flattened config keys} u {#[secret] values}) and errors on
duplicates.
- Spin KV TTL: BoundKvStore exposes put_*_with_ttl (verified in
key_value_store.rs). On Spin these return a deterministic
KvError::Unsupported, never silent store-without-expiry.
- Spin KV listing-cap error variant flagged as an open reconciliation
point with PR #253 (Validation -> a limit/server error); resolved in
commit 2, not a blocker.
- Single (adapter, kind) per-id mapping blocks are now FORBIDDEN
(validation error), not "accepted but vestigial". Fixes the §1 vs
§6.6 contradiction.
- Spin variable naming rule pinned as Spin's own ^[a-z][a-z0-9_]*$
(cites spinframework.dev/manifest-reference), not an EdgeZero rule.
app-demo (§15) updated: manually declares Spin secret variables,
single-component spin.toml, asserts Spin provision writes only
key_value_stores and config push writes both spin.toml tables.
* Seventh-pass review + dev→demo rename + documentation step
Seventh-pass review fixes (against 27a6169):
- KvError::Unsupported does not exist today — spec now states commit 2
adds the variant with a 5xx-class EdgeError mapping (Spin TTL writes).
- Spin listing-cap error resolved in-spec, not left open: commit 2 adds
KvError::LimitExceeded (5xx-class), and the Spin listing path returns
it past max_list_keys, replacing PR #253's KvError::Validation.
- run_dev() -> ! corrected: the dev server may return. Now
run_demo() -> Result<(), String>; commit 1 adjusts the dev-server
boundary (today it returns ()).
- Commit 2 bisectability: added a config-seeding story — the axum
config store's backing-file contract lands in commit 2, but commit-2
tests seed the .edgezero/local-config-<id>.json fixture directly;
config push / demo-regeneration that produce the file land in
commits 7/8.
- Spin config/secret collision check clarified as typed-only
(needs AppConfigMeta::SECRET_FIELDS); raw validation does the
key-syntax and component-discovery checks but not the collision
check, and says so in its diagnostics.
- Spin variable-name rule kept pinned to spinframework.dev docs.
dev → demo subcommand rename (per user):
- The subcommand that runs the example app locally on axum is now
`demo`; `dev` is reserved for a future dev-workflow command.
- run_dev → run_demo, Command::Dev → Command::Demo, the CLI's
dev_server module → demo_server. The edgezero-adapter-axum crate's
own internal dev_server module is left as-is (not user-facing).
Documentation update step (per user):
- New §6.12 makes documentation part of every commit's
definition-of-done, with a page→commit ownership table
(cli-reference, configuration, kv, handlers, getting-started,
adapters/cloudflare, adapters/overview, architecture).
- Commit 8 ends with a documentation audit: grep docs/ for stale
references (old manifest keys, dev subcommand, old store API),
confirm none remain, confirm the .vitepress/config.mts sidebar is
complete, docs CI green.
* Eighth-pass review: three minor fixes (no blockers remain)
- Commit 2 bisectability vs AppDemoConfig: §8 now states commit 2's
app-demo handler migration is store-accessor-only (ctx.kv_store(id),
config_store, the refactored extractors). AppDemoConfig and any
typed-app-config handler work are commit 3 — commit 2 never
references a type that lands in commit 3.
- #[secret(store_ref)] vs Single-secrets capability: §6.8 spells out
that axum/cloudflare/spin are all Single for secrets, so any app
including one of them has exactly one secrets id, and every
#[secret(store_ref)] field must resolve to it. store_ref only buys
multiple secret stores on a Fastly-only project. §15 / the
walkthrough show this for the all-four-adapter app-demo.
- Spin variable-name rule drift guard: commit 7 gets a golden-file
test on the generated spin.toml — asserts every variable name
matches ^[a-z][a-z0-9_]*$ and that the generated manifest parses
(round-trips through the same parser the runtime uses), so the rule
cannot drift from Spin's actual manifest behaviour.
Reviewer confirms no blocking design issues remain.
* Ninth-pass review: three minor notes (reviewer sign-off, no blockers)
- Spin manifest validation strength: the spin.toml golden test now
specifies a strongest-first ladder — (1) the spin CLI's own manifest
validation when present (the wasm32 spin CI job already installs it),
(2) a spin_sdk validation entry point if exposed, (3) toml + regex
as the weakest acceptable fallback. The regex is the floor, not the
ceiling; real Spin validation is preferred wherever reachable.
- Generated template vs app-demo example made explicit: `edgezero new`
scaffolds the common case — greeting, nested service section, a
single plain #[secret] — and deliberately does NOT include
#[secret(store_ref)] (a commented line shows how to add it).
store_ref only helps Fastly-only projects, so it should not be the
default in every fresh scaffold. app-demo remains the full-capability
showcase that exercises both secret forms.
- Commit 2 flagged as the explicit review hotspot in §16: the atomic
manifest+runtime rewrite warrants the most reviewer attention; its
per-adapter contract tests are the primary mitigation and should be
reviewed alongside the code.
Reviewer confirms no blocking issues; spec is implementation-ready.
* Add implementation plan for CLI extensions (8-commit PR)
* Fix six plan-review findings (plan + spec)
- Spin config-push --dry-run never mutates: plan Task 8.1 and spec §15
reworded — dry-run PRINTS the would-be both-table content and the
test asserts spin.toml is unchanged on disk. (The real push writing
both tables is covered by commit 7's non-dry-run tests.)
- Spin `component` field location: it belongs on the
[adapters.<x>.adapter] definition struct (with `crate`/`manifest`),
not the top-level ManifestAdapter — otherwise the accepted TOML
would wrongly be [adapters.spin] component = ...
- load_app_config API made consistent: AppConfigLoadOptions
{ env_overlay } struct; simple load_app_config / _raw apply the
overlay (default); load_app_config_with_options / _raw_with_options
take the struct; --no-env calls the _with_options form with
env_overlay: false. No hidden bool param. Updated spec §4 + §6.10
and plan Tasks 3.1 / 3.3.
- Axum multi-KV path rule: one redb file per logical id, file stem
from [adapters.axum.stores.kv.<id>].name -> .edgezero/kv-<name>.redb.
Prevents multi-store collapsing into one backing file.
- Generator manual check: stop assuming the project lands in CWD or
/tmp/throwaway; generate into an explicit mktemp dir via --dir.
- Removed references to a non-existent crates/edgezero-core/src/
hooks.rs — Hooks + ConfigStoreMetadata both live in app.rs.
* Tighten plan: four review findings before execution
- Macro compile-fail tests: Task 3.2 now adds `trybuild = "1"` to
edgezero-macros [dev-dependencies] explicitly (only `tempfile` was
there), with a tests/ui/*.rs fixture + .stderr golden per rejected
case.
- External-consumer test env guard: tests/lib_consumer.rs must restore
EDGEZERO_MANIFEST via an RAII EnvOverride guard and stay a single
#[test] (no in-binary parallelism); a shared Mutex guard is required
if more env-touching tests are ever added.
- WASM contract test commands pinned: Task 2.7 step 6 names the exact
target / features / runner per adapter (cloudflare wasm32-unknown-
unknown + wasm-bindgen; fastly wasm32-wasip1 + Viceroy; spin
wasm32-wasip1 + Wasmtime), deferring to test.yml as source of truth.
- app-demo e2e lifecycle: Task 8.1/8.2 now require an ephemeral port
(no hard-coded 8787), a readiness poll (no bare sleep), and RAII
teardown that kills the demo server even on assertion failure;
the loop is preferably a Rust integration test, not shell-in-YAML.
* Plan: wire new commands into the default binary, upgrade scaffold, align gate
- Default `edgezero` binary wiring (High): commits 4-7 now have explicit
steps to add Auth / Provision / Config(Validate|Push) to the default
edgezero-cli `Command` enum and `main.rs` dispatch (raw run_* — the
default binary has no app struct), with `edgezero --help` / parse
tests. Previously only the original five commands and app-demo-cli
were wired; the spec requires the new subcommands on the default
binary too. New Task 4.2 covers `config`; Task 5.2/6.1/7.2 extended.
- Generated `<name>-cli` template upgrade (Medium): new Task 8.2 updates
templates/cli/src/main.rs.hbs to the full eight-command set once
auth/provision/config exist, wiring the scaffold's config arm to the
typed functions with the generated project's config struct. Generator
test asserts it.
- Full-gate alignment (Medium): added a canonical "## The full gate"
section with the exact five CI commands from CLAUDE.md / the
workflows (cargo check uses --features "fastly cloudflare spin", not
--all-features). Every "run the full gate" step references it; fixed
the commit-1 and commit-8 gate steps and the Codebase-facts CI line
that had drifted to --all-features.
Commit-8 tasks renumbered (8.2 CI wiring -> 8.3; walkthrough/audit -> 8.4).
* Plan: fix three crate-dependency gaps for typed-config wiring
- app-demo-cli missing app-demo-core dep (High): Task 4.3 now adds
`app-demo-core = { path = "../app-demo-core" }` to
app-demo-cli/Cargo.toml — it references AppDemoConfig once typed
`config validate` / `config push` are wired, but its deps were only
edgezero-cli/clap/log.
- Generated <name>-cli template missing core-crate dep (High): Task 8.2
now also updates templates/cli/Cargo.toml.hbs to depend on
`{{name}}-core` (path dep), and the generator test asserts the
scaffold builds with that dependency and resolves the typed config
type.
- AppConfig macro + validator availability (Medium): chosen route
stated explicitly — `edgezero-core` re-exports the `AppConfig` derive
(matching the existing `action`/`app` re-exports), so a config crate
needs only `edgezero-core` for the macro, no direct edgezero-macros
dep. Task 3.4 updates templates/core/Cargo.toml.hbs to add
`validator` (with derive); Task 3.5 verifies app-demo-core already
carries edgezero-core + validator + serde. Generator test checks the
scaffolded core crate builds.
Task 3.4 / 4.3 / 8.2 steps renumbered to fit the inserted dependency
steps.
* Plan: generator context for config type name + validator workspace seed
- Generated config type placeholder (Medium): Task 3.4 step 1 now
explicitly adds a `NameUpperCamel` key to the generator Handlebars
context (derived from `name`: split on -/_, upper-case each segment,
join — `my-app` -> `MyApp`), with a unit test. Templates reference
`{{NameUpperCamel}}Config`; the key was previously unset (generator
data only had name/proj_core/proj_core_mod/proj_mod).
- validator workspace-dep plumbing (Medium/Low): Task 3.4 step 3 now
names the generator change explicitly — `templates/core/Cargo.toml.hbs`
uses `validator = { workspace = true }`, so `validator` must also be
added to the generator's workspace-dependency seed
(`seed_workspace_dependencies` in generator.rs), which omits it today.
- Duplicate Step 4 in Task 3.4 (Low): Task 3.4 renumbered cleanly to
Steps 1-6.
* Plan: fix generated-CLI import path + guarantee valid NameUpperCamel ident
- Generated CLI import (Medium): the cli template's `use` must
reference the core crate's Rust module name, not the package name.
`use {{name}}_core::...` renders `my-app_core` for `my-app` (invalid
Rust). Task 8.2 now uses `{{proj_core_mod}}` — the hyphen-to-
underscore module form the generator already exposes.
- NameUpperCamel validity (Medium/Low): Task 3.4 step 1 derivation now
guarantees a valid Rust type identifier — derive from the sanitized
crate name, drop empty segments (absorbs a leading `_`), and prefix
with `App` when the result would start with a non-letter (digit-
leading project names). Unit test covers `123-app` -> `App123App`,
`_foo` -> `Foo`, etc.
* Fixed formatting
* Address PR review: stale scaffold deps + KV pagination scan-cap bug
ChristianPavilonis review:
* Generator dependency seeds were stale relative to the adapters:
worker 0.7 → 0.8
fastly 0.11 → 0.12
simple_logger 4 → 5
Scaffolded projects pinned older provider SDKs than the adapters
expect, risking public-type mismatches in generated entrypoints.
* `PersistentKvStore::list_keys_page` lost keys when the scan cap was
hit. On a cap-hit the loop broke with `reached_end = false`, but the
cursor was only emitted when `live_keys.len() > limit`. An
under-filled page (cap reached while skipping a long expired run)
therefore returned `cursor: None` and callers stopped paginating,
silently missing live keys past the expired run. Now a cap-hit is
tracked and the last scanned key is returned as the resume cursor.
Added `list_keys_page_returns_resume_cursor_when_scan_cap_is_hit`.
`LIST_SCAN_BATCH_SIZE`/`MAX_SCAN_BATCHES` are lowered under
`cfg(test)` so the cap path is reachable with a 43-entry fixture
instead of 25k; pagination correctness is batch-size-independent.
* Commit 1: extensible edgezero-cli library + generator + app-demo-cli
Turn edgezero-cli into lib + bin so downstream projects can build their
own CLI binary reusing any subset of the built-in commands.
- Promote Command variant fields into standalone #[derive(clap::Args)]
structs (BuildArgs / DeployArgs / ServeArgs; NewArgs already standalone),
each #[non_exhaustive] + Default for external construction.
- Add src/lib.rs exposing the public API: run_build / run_deploy /
run_serve / run_new / run_demo, init_cli_logger, and the args module
(pub mod, not pub use — restriction lint). main.rs becomes a thin
wrapper over the library.
- Rename the `dev` subcommand to `demo` (dev is reserved for a future
dev-workflow command): dev_server.rs -> demo_server.rs, run_dev ->
run_demo (now Result<(), String>), Command::Dev -> Command::Demo.
- Extend the generator to scaffold a crates/<name>-cli crate from new
templates/cli/ Handlebars templates; seed clap + edgezero-cli as
workspace dependencies; add crates/<name>-cli to the workspace members.
- Add the handwritten examples/app-demo/crates/app-demo-cli crate as the
canonical downstream consumer, with a --help smoke test.
- Add crates/edgezero-cli/tests/lib_consumer.rs: external-consumer
integration test proving the public API is usable from outside.
- Docs: cli-reference.md (demo rename + "Building Your Own CLI"),
getting-started.md, CLAUDE.md.
All gates green: fmt, clippy -D warnings, cargo test --workspace,
feature cargo check, spin wasm32; app-demo workspace fmt/clippy/test.
* Make `demo` example-only; `serve --adapter axum` runs the axum adapter
After the dev->demo rename, `demo` should mean "run the bundled example",
not "run the project's axum adapter". Drop `try_run_manifest_axum` (and
its `load_manifest_optional` helper) from `demo_server`: `edgezero demo`
now always starts the built-in example server on 127.0.0.1:8787 and never
reads `edgezero.toml`.
`edgezero serve --adapter axum` is now the single, unambiguous way to run
a project's axum adapter (it runs `[adapters.axum.commands].serve`). This
removes the demo / serve --adapter axum behavioral overlap. Docs updated.
* Plan: mark Commit 1 done, fix stale branch/path, expand Fastly in Commit 2
- Commit 1 marked DONE (landed 1d582dd + follow-up 06f4b72) with a
Status section, so workers don't redo already-landed work.
- Working-branch reference corrected: feature/extensible-cli (was the
stale docs/extensible-cli-library-spec).
- app-demo edgezero-cli dep path fixed to ../../crates/edgezero-cli
(relative to the workspace manifest; the four-up path was wrong and
would break the demo workspace).
- Task 2.7 Fastly step expanded from one line to explicit per-kind
registry steps + contract tests: Fastly is Multi for KV/config/
secrets, two logical stores per kind, per-id name resolution,
id-keyed contract coverage under Viceroy — parity with the
cloudflare/spin acceptance criteria.
* Spec: document the namespaced args API (edgezero_cli::args::*)
Commit 1 shipped `pub mod args` rather than crate-root re-exports: a
root `pub use args::{...}` trips clippy::pub_use (the restriction group
is -D-denied workspace-wide). §4 now documents the supported API as
edgezero_cli::args::BuildArgs etc., with run_* staying at the crate
root, and updates every run_* signature to &args::<T>. Matches what
1d582dd actually exposes and what lib_consumer.rs / cli-reference.md
already use.
(Reviewer's second finding — demo overlapping serve --adapter axum in
1d582dd — was already resolved by 06f4b72; no action.)
* Address PR review: scaffold templates fail their own clippy/test gate
Review comment #7: a freshly generated project failed its own
restriction-deny clippy gate immediately.
- Core handler template: mirror app-demo's passing structure —
fallible `stream`/`IntoResponse` usage (no production `.expect`),
alphabetically ordered structs, grouped test items, `IntoResponse`
imported anonymously.
- Adapter host stubs: add `#[expect(clippy::print_stderr, reason)]`
and `allow(dead_code, reason)`; the axum entrypoint returns
`anyhow::Result` instead of `eprintln!` + `process::exit`.
- Inline the project-core `App` type in adapter entrypoints so import
order stays stable regardless of project name.
- key_value_store: replace `#[cfg(not(test))]` consts with
`if cfg!(test)` and rename a `cursor` binding that shadowed the
parameter (clippy `cfg_not_test` / `shadow_unrelated`).
- Add scaffold lint-coverage assertions to the generator test.
* demo: run app-demo via run_app for full manifest setup
`edgezero demo` now delegates to `edgezero_adapter_axum::dev_server::run_app`,
running the bundled app-demo example the same way its own axum adapter does.
This wires the complete manifest setup (routing, KV/config/secret stores,
logging, host/port) instead of a hand-rolled echo router.
The demo path requires the `dev-example` feature; without it `run_demo`
returns an actionable error.
* Plan/spec: rename "Commit N" to "Stage N"
The eight numbered work units are now "stages" rather than "commits" —
each stage may span multiple git commits. Literal git-commit actions
(commit steps, `git commit -m`, the PR head commit) keep the "commit"
wording.
* Formatting
* Make demo a contributor-only command; rename feature to demo-example
Addresses review findings on the demo subcommand:
- demo is exposed only when built with the new `demo-example` feature.
Generated CLIs and app-demo-cli no longer expose `Demo` at all — a
downstream project has no bundled app-demo to run. The default
`edgezero` binary gates `Command::Demo` on `demo-example`, so the
advertised `--help` surface matches what actually works.
- `demo-example` (renamed from `dev-example`) now also pulls in
`edgezero-adapter-axum`, making the feature self-contained.
- getting-started.md points generated projects at
`edgezero serve --adapter axum`; cli-reference.md documents `demo`
as contributor-only.
- NewArgs now derives Default and is #[non_exhaustive], matching the
other public *Args structs.
- Generated handler tests serialize API_BASE_URL access behind a
mutex + RAII env guard.
- Refreshed README, CLAUDE.md, architecture docs, and agent docs for
the dev->demo / dev-example->demo-example rename.
* Fix binary name, stale dev docs, and scaffold drift
Addresses review findings on the Stage 1 surface:
- Add a `[[bin]] name = "edgezero"` target so `cargo build` produces
`target/debug/edgezero` — the name every doc and the clap `about`
already use.
- Remove the inert `--local-core` flag from `NewArgs`; it was never
read by the generator.
- Warn when `edgezero new` falls back to a Git dependency for
`edgezero-cli`: the generated CLI crate needs `edgezero-cli` as a
published library, so an out-of-repo scaffold only builds once that
is available on the referenced remote. In-repo generation uses a
path dependency and is unaffected.
- Replace removed `edgezero dev` references with
`edgezero serve --adapter axum` in the root README, architecture,
and axum adapter docs.
- Drop `run_demo` from the "build your own CLI" surface (it is
contributor-only), and add the generated `*-cli` and Spin adapter
crates to the scaffold structure docs.
* Generate path dependencies to the local edgezero checkout
Fixes fresh `edgezero new` projects failing to build outside the repo.
The generated CLI crate imports `edgezero_cli`, but dependency
resolution fell back to a Git dependency whenever the output directory
was outside the repo root — and the published `edgezero-cli` has no
library target, so every `edgezero_cli::...` import failed.
- Locate the edgezero checkout via `CARGO_MANIFEST_DIR` (baked in at
build time) instead of the current directory, so generation finds the
checkout regardless of where the project is created or where the
command runs.
- When the output directory is outside the checkout, emit an absolute
path dependency rather than the Git fallback. The Git fallback now
only applies to a binary detached from its source tree.
- Assert in the generator test that the scaffold resolves edgezero
crates to path dependencies, so a regression to the Git fallback is
caught by `cargo test -p edgezero-cli`.
- Add an opt-in (`#[ignore]`) integration test that runs `cargo check`
on the generated CLI crate, proving it compiles against the local
`edgezero-cli` library.
- Drop the stale `--local-core` option from the CLI reference docs.
* Verify the full generated workspace compiles, not just the CLI crate
Broaden the opt-in scaffold test to `cargo check --workspace` and drop
`--offline`: a freshly generated project has no lockfile, so offline
resolution of transitive registry crates is unreliable (true of any
scaffolded project). Online, the full generated workspace compiles.
* Fix generated-README serve command; align plan/spec with 4-command CLI
- Adapter README `dev_steps` snippets advised `edgezero-cli serve
--adapter ...`, but the binary is `edgezero` (the `edgezero-cli`
package builds `target/debug/edgezero`). Corrected all four adapters
(axum, cloudflare, fastly, spin) so generated-project READMEs show a
working command.
- Updated the plan and spec acceptance notes: generated and app-demo
CLIs expose the four downstream built-ins (build/deploy/new/serve),
not five — `demo` is contributor-only and absent from downstream
CLIs. Also corrected the Stage 8 generated-CLI command count.
* Drop redundant cfg attributes in spin KV/secret store modules
The key_value_store and secret_store modules are already gated at their
mod declaration in lib.rs (#[cfg(all(feature = "spin", target_arch =
"wasm32"))]), so every per-item copy of that same cfg inside the files
was a tautology. Removing the 14 no-op attributes makes both files
consistent with their sibling request.rs/response.rs/proxy.rs, which
already rely on the gated mod declaration.
* Wire generated-project compile check into CI; fix stale plan lines
- Add a CI step that runs the `generated_project_builds` test
(`-- --ignored`), so the Stage 1 scaffold regression — a fresh
`edgezero new` project failing to compile — is caught by CI rather
than only by manual runs.
- Correct two stale Stage 1 plan steps: a default `cargo build
-p edgezero-cli` exposes four subcommands, not five; `demo` is gated
behind the `demo-example` feature.
* Fix generated wasm adapters and project-name sanitisation
Stage 1 review findings on generated projects:
- Cloudflare adapter template called `run_app(req, env, ctx)` but the
API takes `manifest_src` first — generated Cloudflare crates failed
to compile for wasm32. Aligned the template with the other three
adapters and the handwritten app-demo crate.
- The Spin `#[http_component]` macro expands to an unsafe wasm export,
which trips the generated workspace's `unsafe_code = "deny"` gate.
Added a narrow wasm-only `#[allow(unsafe_code)]` with a reason to the
Spin entrypoint, in the template and in app-demo.
- `sanitize_crate_name` mangled uppercase letters to `-`, so
`edgezero new MyApp` produced the invalid package name `-y-pp-core`.
It now lower-cases ASCII letters, keeps `-`/`_`, collapses other
characters, and trims leading/trailing separators; added unit tests.
- The opt-in `generated_project_builds` test only checked the host
target. It now also runs `cargo check` for each adapter's wasm
target (skipping a target that is not installed), which is where the
two failures above lived.
Plan: marked PR #253 merged, and recorded two post-review Stage 2
design inputs — downstream binaries must build without an
`edgezero.toml`, and the manifest holds only non-adapter-specific
config.
* Plan: clear stale PR #253 gating and five-built-ins references
- Status block no longer calls Stage 2 "gated on PR #253" — the
precondition is met (PR #253 merged).
- Task 8.2 now says Stage 1 created the generated CLI template with
four downstream built-ins, not five (demo is contributor-only).
* Spec/plan: revise Stage 2 to the portable-manifest + EDGEZERO__ design
Reworks spec §6.6/§8 and the plan's Stage 2 tasks for the design agreed
in review:
- edgezero.toml is portable and non-adapter-specific — [app], routes,
[environment], and [stores.<kind>] logical ids/default only. No
[adapters.*] table.
- The manifest is never compiled into the binary; the app! macro bakes
the portable config into the App/Hooks type at compile time, and
run_app::<A>() drops its manifest_src parameter (no include_str!).
- Adapter-specific runtime config — store platform names, tuning,
host/port, logging — comes from EDGEZERO__* environment variables at
runtime, with defaults when absent.
- An adapter binary builds and runs with no edgezero.toml and zero env
vars.
Plan Task 2.1–2.9 rewritten accordingly (adds the EDGEZERO__ env-config
layer task; drops the in-manifest per-adapter mapping).
* Stage 2 Task 2.1: portable manifest store schema
Rewrites the manifest store model to the §6.6 portable schema:
- `[stores.<kind>]` now carries only logical `ids` (non-empty) and an
optional `default` (required when >1 id, must be a declared id). The
five per-adapter store config types collapse into one reusable
`StoreDeclaration`.
- The pre-rewrite store schema (`[stores.<kind>] name`,
`[stores.config.defaults]`, `[stores.<kind>.adapters.*]`, `enabled`)
is a hard load error whose message points at the migration guide.
- Store helper methods resolve a store's name to its logical default
id (interim — `EDGEZERO__*` env overrides arrive in Task 2.2).
- `[stores.config.defaults]` and its axum dev-server seeding are gone.
- Migrated `examples/app-demo/edgezero.toml` and the generated
`edgezero.toml.hbs` template to the new schema.
Scoped to store types only; `[adapters.*]`, the env layer, and adapter
store registries are later Stage 2 tasks.
* Stage 2 Task 2.2: EDGEZERO__* environment-config layer
New `edgezero-core::env_config` module: parses `EDGEZERO__`-prefixed
environment variables (`__` = key-path separator, segments lower-cased)
into an `EnvConfig` value with accessors for store platform names +
tuning, bind host/port, and logging level.
- `from_env()` reads the process environment; `from_vars()` lets the
Cloudflare adapter supply its `Env` binding (no `std::env` there).
- `store_name(kind, id)` falls back to the logical id when unset.
Additive only — wired into the runtime in later Stage 2 tasks.
* Stage 2 Tasks 2.3 + 2.4: bake portable stores into Hooks; drop manifest_src from run_app
Couples the macro/runtime change with the adapter signature change so the
workspace and `examples/app-demo` stay buildable in a single commit.
- `Hooks::stores() -> StoresMetadata` replaces `config_store()`. The
`app!` macro emits portable `StoreMetadata { default, ids }` for
`[stores.config|kv|secrets]`. `Hooks::stores()` defaults to empty so
apps built without the macro still compile.
- `run_app::<A>()` no longer takes `manifest_src` on any adapter — axum,
cloudflare, fastly, spin. Each reads `A::stores()` and layers
`EDGEZERO__*` env config on top (logging level, bind host/port, store
platform names). All four entrypoint templates, all four
`app-demo-adapter-*` consumers, and `edgezero-cli/src/demo_server.rs`
drop `include_str!("edgezero.toml")`.
- axum `resolve_addr` reads `EDGEZERO__ADAPTER__HOST`/`PORT` only; the
`[adapters.axum.adapter]` fallback is gone (consistent with the §6.6
no-runtime-tables rule).
- cloudflare derives the exact `EDGEZERO__STORES__<KIND>__<ID>__NAME`
keys from baked metadata to query the worker `Env` (workers cannot
enumerate). Deprecated `run_app_with_manifest` is removed.
- spin drops `dispatch_with_manifest`; the KV label resolves from
`EDGEZERO__STORES__KV__<ID>__NAME` or the declared default id.
All five CI gates green + `examples/app-demo` tests pass.
Tasks 2.5–2.9 (async ConfigStore, store registries, extractors,
app-demo/templates/docs migration, ship gate) follow.
* Stage 2 Task 2.5: async ConfigStore, new KvError variants, store registry, id-keyed RequestContext
Lands the runtime-API shape for §6.6 multi-store support. No adapter
yet builds a `StoreRegistry` — that arrives in Task 2.6. The new
RequestContext accessors are wired with a legacy single-handle
fallback so all four adapters keep compiling and tests stay green
through the transition.
- `ConfigStore::get` → `async` (`#[async_trait(?Send)]`). Required for
Cloudflare's KV-backed config store (§8 Task 2.6) which is async at
the SDK boundary. All four adapter impls + `FixedConfigStore` test
doubles + app-demo's `MapConfigStore` / `UnavailableConfigStore` are
updated; the contract-test macro switches to `block_on` (same pattern
as the KV contract macro). `ConfigStoreHandle::get` follows.
- New `KvError` variants with `From<KvError> for EdgeError` mappings:
- `Unsupported { operation }` → `EdgeError::not_implemented` (501).
Used by Spin TTL writes (§6.7), where `key_value::Store::set` has
no expiry parameter.
- `LimitExceeded { message }` → `service_unavailable` (503). Used by
Spin's `get_keys` cap (`max…
aram356 added a commit that referenced this pull request Jul 7, 2026
…ecker, AdapterPushContext, run_native_cli, compression) (#285)
* Document why exhaustive_enums stays as workspace allow
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.
* Remove missing_inline_in_public_items allow; add #[inline] to ~321 fns
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.
* Rename Manifest::secret_store_name → secret_store_binding
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`.
* Bump checkout/setup-node/cache actions v4 → v5 (Node 24 runtime)
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).
* Bump remaining CI actions to current latest majors
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)
* Upgrade redb 4.0 → 4.1
* Fix CodeQL rust/cleartext-logging by dropping binding name from log
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.
* Fix CodeQL rust/cleartext-transmission (#9, #10): rename test helper
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.
* Add tests for behaviour added in this PR
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.
* Upgrade ctor 0.10 → 1.0; document spin-sdk 6.0 MSRV gap
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.
* Upgrade toolchain to rust 1.95.0; bump viceroy to 0.17
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
* Fix two clippy warnings only visible on no-features build
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.
* Pull edgezero_adapter_axum::dev_server::run_app via use in app-demo
* Fix spin CI: pin wasmtime via .tool-versions + direct GitHub tarball
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.
* Address PR review comments
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.
* Surface invalid handler paths via compile_error! instead of panicking
`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`.
* Document pub_with_shorthand with verbatim clippy diagnostic
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.
* Add design spec for extensible edgezero-cli library
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.
* Expand CLI extensions spec to cover all 7 sub-projects
Replaces the sub-project-#1-only spec with a single design document that
covers the full effort: extensible edgezero-cli library, generator updates
for <name>-cli and <name>.toml scaffolding, per-service typed app-config
schema with validator integration, four new commands (auth, provision,
config validate, config push), shell-out mocking via a private
CommandRunner trait, and the app-demo overhaul that exercises everything
end-to-end.
Implementation still ships in 7 incremental PRs but the design decisions
live in one place so reviewers see the whole picture.
Force-added because docs/superpowers/ is gitignored project-wide.
* Apply review feedback and add secret annotation to CLI extensions spec
High-severity fixes:
- Add --manifest to ProvisionArgs and ConfigPushArgs (matches validate)
- Update Wrangler invocations to 3.60+ syntax (space-form, --namespace-id)
- Persist provisioned IDs in edgezero.toml [stores.*.adapters.<x>].id;
cross-write to per-adapter manifests where deploys need them
- Mermaid diagram in §3 replacing ASCII art
Medium-severity fixes:
- config push runs strict validation as pre-flight (no separate flag)
- Move --adapter to each AuthSub variant so UX is `auth login --adapter X`
- Constrain typed config push to serde_json::to_value(C) -> Object;
document flatten / rename / skip / Option::None handling
- Unify raw + typed serialization rules; raw drops Validate + secret skip
- Replace CommandRunner positional args with CommandSpec struct
(program, args, cwd, stdin, env)
- "Backwards-compatible" language replacing "unchanged" for default bin
- Move walkthrough doc to docs/guide/ with explicit sidebar update
Low + open questions:
- Document consumer-facing Cargo feature names and adapter opt-outs
- Generator migration note: sub-project 1 outputs don't auto-migrate
- Deprecate [stores.config.defaults] in favor of <name>.toml [config]
- Mark Spin provision / config push as "not yet supported" with pointer
to the in-flight Spin stores PR; clear error message until then
Secret annotation:
- New §6.6 documenting #[derive(AppConfig)] from edgezero-macros
- #[secret] field attribute marks runtime-secret-store-backed fields
- Toml value for those fields is the secret-store binding name
- config validate (typed) cross-checks the binding appears in [stores.secrets]
- config push (typed) skips SECRET_FIELDS entirely
The implementation still ships in 7 incremental PRs.
* Expand spec for multi-store manifest + finalize naming and validate scope
Manifest schema rewrite (new sub-projects #2 and #3):
- [stores.<kind>].ids = [...] + default declare the logical stores the
app uses (kv, secrets, config all multi-store)
- [adapters.<X>.stores.<kind>.<id>].name = "..." maps each logical id to
the platform-specific name on adapter X, with optional adapter-specific
tuning fields stored as free-form extras
- Provisioned platform resource IDs (Cloudflare namespace ID, Fastly
store ID) live in each platform's native manifest (wrangler.toml,
fastly.toml), not in edgezero.toml. provision writes them there;
config push reads them back.
- RequestContext store accessors become id-keyed:
ctx.kv_store("id") / ctx.kv_store_default() (and similarly for
config_store / secret_store). Each adapter builds a StoreRegistry<H>
at request setup from [adapters.<self>.stores.*].
- Manifest validator enforces: ids non-empty; default in ids;
every adapter has a name mapping for every id.
Naming:
- Field on the per-adapter block is `name` (matches the user's example),
not `binding`. The Cloudflare wrangler.toml term `binding` is now
called out as wrangler's terminology, not ours.
Secret references (§6.7):
- The string a #[secret] field holds is an app-defined reference; the
spec documents both valid runtime patterns (logical store id or key
within the default secret store). Validate just confirms the string
is non-empty and that the app has a secret store available.
config validate (§11) explicitly covers app-config validation:
- TOML syntax, [config] table presence, type matching against C,
serde-rejected unknown fields, validator business rules, non-empty
secret references, and the manifest-side cross-checks.
Sub-project count: 7 → 9 (added schema rewrite + RequestContext API
rewrite as #2 and #3; existing app-config/validate/auth/provision/push/
polish become #4-#9).
This is a breaking change to the on-disk manifest schema; the in-tree
example/app-demo is migrated as part of the work, and a migration guide
ships with sub-project #2.
* Apply second-pass review: runtime API completeness, Cloudflare KV, secret forms
HIGH severity fixes:
- Cloudflare config store rewritten from [vars] to KV (§6.9) so
`config push` actually reaches the runtime without redeploying.
Lands in sub-project #3 alongside the rest of the runtime work.
- Sub-project #2 is now purely additive on the schema: no runtime
changes, no removal of [stores.config.defaults]. The runtime bridge
and the defaults removal move out of #2 (into #3 and #9 respectively).
- Spin completeness: validator skips adapters without an
[adapters.<X>.stores] section. App-demo's Spin adapter omits stores
until the in-flight Spin stores PR lands.
- Extractor design (§6.8): existing Kv / Secrets extractors keep
working as default-store accessors; new KvNamed<const ID> /
SecretsNamed<const ID> extractors give type-safe named access. No
handler-facing break.
- Hooks, ConfigStoreMetadata, and app! macro added to sub-project #3
scope; they all become id-keyed. Multi-store rewrite is now complete.
MEDIUM severity fixes:
- Validate bound is DeserializeOwned + Validate + AppConfigMeta (no
Serialize). The serde_json::to_value object check is push-only;
push adds Serialize.
- Secret semantics: two explicit forms via attribute. #[secret] = key
inside the default secret store. #[secret(store_ref)] = logical store
id in [stores.secrets].ids. Validate cross-checks the latter.
- AppConfigMeta::SECRET_FIELDS is now &'static [SecretField] carrying
SecretKind so the CLI can apply the right validation per field.
- #[secret] constrained to non-flattened, non-renamed scalar fields;
combinations with #[serde(flatten)] / rename / skip produce compile
errors. Macro tests cover the constraints.
- Unknown-field rejection is no longer a validate guarantee; the
generator template emits #[serde(deny_unknown_fields)] on the
generated config struct so new projects opt in by default.
- Every public *Args derives Default + #[non_exhaustive]; external
construction documented as Default + field mutation.
LOW severity fixes:
- Macro example fixed: #[proc_macro_derive(AppConfig, attributes(
secret))] in edgezero-macros/src/lib.rs directly. No bogus _impl
re-export.
- Cloudflare-invalid JS-identifier `name` values are errors (would
break worker deploy), not warnings.
Sub-project ordering and risk:
- #2 risk dropped to L (purely additive).
- #3 grows to absorb Cloudflare KV swap + Hooks/macro/extractor.
- #9 now also drops [stores.config.defaults] and wires axum dev-server
to seed from <name>.toml.
* Third-pass review: async ConfigStore, env overlay, extractor refactor
HIGH severity fixes:
- ConfigStore::get becomes async (#[async_trait(?Send)]). Cloudflare
config moves [vars] -> KV with real async reads. Cascade (trait, 3
adapter impls, Hooks, handlers, extractors) contained to #3.
- Drop const-generic &'static str extractors (don't compile on stable
1.95). Kv / Secrets extractors refactored to yield a registry handle
with default() / named(id) accessors.
- Introduce BoundKvStore / BoundConfigStore / BoundSecretStore so
runtime accessors return a handle bound to the resolved platform
name; callers just .get(key).await.
- Sub-project #2 models logical store declarations as
Option<LogicalStoreConfig> so old-shape manifests (None) are
distinguishable from new-but-incomplete ones (Some with empty ids).
Keeps #2 genuinely additive.
MEDIUM severity fixes:
- Fastly native-manifest writeback: spec commits to a read/write-path-
agreement contract; exact fastly.toml sections pinned in #7's plan.
- Adapter store completeness uses an explicit
STORES_SUPPORTED_ADAPTERS allowlist (axum, cloudflare, fastly). A
supported adapter omitting [adapters.<X>.stores] is an error; only
non-allowlisted adapters (spin) skip.
- All "default store" prose uses the resolved default id (explicit
default, else single ids[0]).
- AuthArgs no longer derives Default (avoids a placeholder subcommand
leaking into a real auth path). §6.11 documents which *Args get
Default.
- config push gains explicit "validate passes, push serialization
fails" test scenarios (non-object typed config, compound shapes,
skip_serializing_if, Option::None, flatten).
LOW severity:
- Ship-gate wording: existing commands stay backwards-compatible
rather than "edgezero --help unchanged" (false once auth/provision/
config land).
New requirement - environment-variable override resolution (§6.10):
- load_app_config overlays env vars on the toml [config] table.
- Env var format: <APP_NAME>__<SECTION>__..__<KEY>; __ separates every
nesting level; APP_NAME is [app].name uppercased, hyphens to
underscores.
- Type coercion against the target TOML type; --no-env escape hatch on
validate and push.
app-demo (§15) now explicitly exercises every new capability: multi-
store, async config, named-kv extractor, nested config section, env
override, both secret forms, validate/push, auth/provision via mock.
* Fourth-pass review: manifest discrimination, Hooks split, env coercion, Fastly contract
HIGH severity fixes:
- Manifest old-vs-new discrimination corrected. Existing manifests
already have [stores.kv/secrets/config] tables, so table-presence
can't discriminate. Sub-project #2 now uses compatibility structs
carrying legacy fields (name, legacy adapters) plus new logical
fields (ids, default) side by side; the discriminator is
ids.is_some(). The current app-demo edgezero.toml parses unchanged.
- Hooks cannot return bound handles. Hooks / ConfigStoreMetadata are
static compile-time app metadata; bound handles need per-request
adapter state. Split: Hooks/app! emit store metadata registries;
only RequestContext returns Bound*Store handles. Adapters consume
the metadata at request setup to build the runtime registries.
- Env overlay type coercion: with C: DeserializeOwned there is no
pre-deserialization type reflection. Env vars now override existing
keys only, coerced to the existing TOML value's type. Matches the
current AxumConfigStore::from_env behavior. To make a key
env-overridable it must appear in <name>.toml.
- Axum config push and runtime read agreed: the axum config store is
backed by .edgezero/local-config-<id>.json; config push --adapter
axum writes that file; edgezero dev regenerates it at startup. No
more disagreement between push target and dev-server source.
MEDIUM severity fixes:
- Fastly writeback contract made concrete from Fastly's docs:
[setup.<kind>_stores.<name>] + [local_server.<kind>_stores.<name>]
keyed by resource link name (== our `name`). provision creates the
store and ensures both fastly.toml sections exist; config push
resolves the store id on demand via `fastly config-store list
--json` (Fastly has no stable persisted id slot). Read/write paths
all key off [adapters.fastly.stores.<kind>.<id>].name.
- Env key matching is deterministic and ambiguity-rejecting: keys
transform to an env segment form (uppercase); two siblings mapping
to the same segment is an AppConfigError. No case-insensitive fuzzy
fallback.
- Cloudflare KV eventual consistency: §6.9 no longer claims values are
live "on the next request"; CI does not assert immediate global
Cloudflare visibility.
LOW severity:
- BoundSecretStore keeps the existing bytes::Bytes API (get ->
Option<Bytes>, require_str), not Vec<u8>.
* Fifth-pass review: hard cutoff, Spin as first-class store adapter, one-PR delivery
Hard cutoff (per user directive — projects fully migrated, no compat):
- Removed all old-vs-new manifest discrimination: no compat structs,
no ids.is_some() check, no legacy-field parsing. The store schema is
rewritten outright. Legacy fields (name, legacy adapters overrides,
[stores.config.defaults]) are hard load errors pointing at the
migration guide.
Spin as a first-class store-capable adapter (PR #253 baseline):
- Removed the "Spin deferred" non-goal. Spin participates fully.
- New §6.7 Spin store semantics: KV is label-backed multi-store with a
max_list_keys cap; config and secrets are both spin_sdk::variables —
a single flat namespace, lowercase [a-z0-9_] keys, no dots.
- Replaced the flat STORES_SUPPORTED_ADAPTERS allowlist with an
adapter x kind capability matrix (Multi vs Single). Validation: if
any target adapter is Single for a kind, [stores.<kind>].ids must
have exactly one id (you cannot have two config stores if you also
target Spin).
- §6.4 config key model: nested config flattens to dotted keys;
canonical handler form is dotted; Spin config store translates
. -> __ internally; config push writes platform-native key form.
- Spin wired into commit 2 (runtime registry, async ConfigStore now
cascades across all FOUR adapters), commit 6 (provision: spin.toml
writeback for key_value_stores / [variables] /
[component.<name>.variables]), commit 7 (config push: Spin variables
in spin.toml).
- provision now has explicit axum (no-op, prints local-store note) and
spin (manifest writeback, no CommandRunner) contracts; config push
is split per adapter — no universal native-resource-ID assumption.
Other review fixes:
- Default resolution made strict: `default` required when ids.len() > 1.
- Docs config path corrected to docs/.vitepress/config.mts (not .ts).
Delivery: one PR with eight commits (one per sub-project), not eight
PRs. CI gates the PR head; each commit should still build for
bisectability. Sub-project count stays at 8 (manifest+runtime stay
merged as the atomic commit 2).
* Sixth-pass review: close Spin integration design holes
Nine findings against the current (f0aed20) spec, all Spin-integration
depth:
- Spin provision cannot know config/secret variable keys (manifest has
store ids, not field keys). Fix: Spin provision does KV-label
spin.toml writeback ONLY. Config-variable declaration moves to
config push (which loads <name>.toml). Secret-variable declaration
is manual.
- config push --adapter spin must write BOTH [variables] (declaration
+ default) and [component.<name>.variables] (binding) — a Spin
variable is unreadable without the component binding. Errors rather
than writing a half-configured manifest.
- Spin component discovery specified: parse spin.toml; single component
resolves implicitly; multi-component requires
[adapters.spin.adapter].component; config validate --strict surfaces
failures early.
- Secret variables are not inferable (#[secret(store_ref)] runtime keys
are code-local). Spin secret variables are declared manually by the
developer; the CLI never writes them.
- Config/secret namespace collision guarantee was wrong: #[secret]
field VALUES (not Rust field names) are the secret keys. config
validate now computes the effective Spin variable set
({flattened config keys} u {#[secret] values}) and errors on
duplicates.
- Spin KV TTL: BoundKvStore exposes put_*_with_ttl (verified in
key_value_store.rs). On Spin these return a deterministic
KvError::Unsupported, never silent store-without-expiry.
- Spin KV listing-cap error variant flagged as an open reconciliation
point with PR #253 (Validation -> a limit/server error); resolved in
commit 2, not a blocker.
- Single (adapter, kind) per-id mapping blocks are now FORBIDDEN
(validation error), not "accepted but vestigial". Fixes the §1 vs
§6.6 contradiction.
- Spin variable naming rule pinned as Spin's own ^[a-z][a-z0-9_]*$
(cites spinframework.dev/manifest-reference), not an EdgeZero rule.
app-demo (§15) updated: manually declares Spin secret variables,
single-component spin.toml, asserts Spin provision writes only
key_value_stores and config push writes both spin.toml tables.
* Seventh-pass review + dev→demo rename + documentation step
Seventh-pass review fixes (against 27a6169):
- KvError::Unsupported does not exist today — spec now states commit 2
adds the variant with a 5xx-class EdgeError mapping (Spin TTL writes).
- Spin listing-cap error resolved in-spec, not left open: commit 2 adds
KvError::LimitExceeded (5xx-class), and the Spin listing path returns
it past max_list_keys, replacing PR #253's KvError::Validation.
- run_dev() -> ! corrected: the dev server may return. Now
run_demo() -> Result<(), String>; commit 1 adjusts the dev-server
boundary (today it returns ()).
- Commit 2 bisectability: added a config-seeding story — the axum
config store's backing-file contract lands in commit 2, but commit-2
tests seed the .edgezero/local-config-<id>.json fixture directly;
config push / demo-regeneration that produce the file land in
commits 7/8.
- Spin config/secret collision check clarified as typed-only
(needs AppConfigMeta::SECRET_FIELDS); raw validation does the
key-syntax and component-discovery checks but not the collision
check, and says so in its diagnostics.
- Spin variable-name rule kept pinned to spinframework.dev docs.
dev → demo subcommand rename (per user):
- The subcommand that runs the example app locally on axum is now
`demo`; `dev` is reserved for a future dev-workflow command.
- run_dev → run_demo, Command::Dev → Command::Demo, the CLI's
dev_server module → demo_server. The edgezero-adapter-axum crate's
own internal dev_server module is left as-is (not user-facing).
Documentation update step (per user):
- New §6.12 makes documentation part of every commit's
definition-of-done, with a page→commit ownership table
(cli-reference, configuration, kv, handlers, getting-started,
adapters/cloudflare, adapters/overview, architecture).
- Commit 8 ends with a documentation audit: grep docs/ for stale
references (old manifest keys, dev subcommand, old store API),
confirm none remain, confirm the .vitepress/config.mts sidebar is
complete, docs CI green.
* Eighth-pass review: three minor fixes (no blockers remain)
- Commit 2 bisectability vs AppDemoConfig: §8 now states commit 2's
app-demo handler migration is store-accessor-only (ctx.kv_store(id),
config_store, the refactored extractors). AppDemoConfig and any
typed-app-config handler work are commit 3 — commit 2 never
references a type that lands in commit 3.
- #[secret(store_ref)] vs Single-secrets capability: §6.8 spells out
that axum/cloudflare/spin are all Single for secrets, so any app
including one of them has exactly one secrets id, and every
#[secret(store_ref)] field must resolve to it. store_ref only buys
multiple secret stores on a Fastly-only project. §15 / the
walkthrough show this for the all-four-adapter app-demo.
- Spin variable-name rule drift guard: commit 7 gets a golden-file
test on the generated spin.toml — asserts every variable name
matches ^[a-z][a-z0-9_]*$ and that the generated manifest parses
(round-trips through the same parser the runtime uses), so the rule
cannot drift from Spin's actual manifest behaviour.
Reviewer confirms no blocking design issues remain.
* Ninth-pass review: three minor notes (reviewer sign-off, no blockers)
- Spin manifest validation strength: the spin.toml golden test now
specifies a strongest-first ladder — (1) the spin CLI's own manifest
validation when present (the wasm32 spin CI job already installs it),
(2) a spin_sdk validation entry point if exposed, (3) toml + regex
as the weakest acceptable fallback. The regex is the floor, not the
ceiling; real Spin validation is preferred wherever reachable.
- Generated template vs app-demo example made explicit: `edgezero new`
scaffolds the common case — greeting, nested service section, a
single plain #[secret] — and deliberately does NOT include
#[secret(store_ref)] (a commented line shows how to add it).
store_ref only helps Fastly-only projects, so it should not be the
default in every fresh scaffold. app-demo remains the full-capability
showcase that exercises both secret forms.
- Commit 2 flagged as the explicit review hotspot in §16: the atomic
manifest+runtime rewrite warrants the most reviewer attention; its
per-adapter contract tests are the primary mitigation and should be
reviewed alongside the code.
Reviewer confirms no blocking issues; spec is implementation-ready.
* Add implementation plan for CLI extensions (8-commit PR)
* Fix six plan-review findings (plan + spec)
- Spin config-push --dry-run never mutates: plan Task 8.1 and spec §15
reworded — dry-run PRINTS the would-be both-table content and the
test asserts spin.toml is unchanged on disk. (The real push writing
both tables is covered by commit 7's non-dry-run tests.)
- Spin `component` field location: it belongs on the
[adapters.<x>.adapter] definition struct (with `crate`/`manifest`),
not the top-level ManifestAdapter — otherwise the accepted TOML
would wrongly be [adapters.spin] component = ...
- load_app_config API made consistent: AppConfigLoadOptions
{ env_overlay } struct; simple load_app_config / _raw apply the
overlay (default); load_app_config_with_options / _raw_with_options
take the struct; --no-env calls the _with_options form with
env_overlay: false. No hidden bool param. Updated spec §4 + §6.10
and plan Tasks 3.1 / 3.3.
- Axum multi-KV path rule: one redb file per logical id, file stem
from [adapters.axum.stores.kv.<id>].name -> .edgezero/kv-<name>.redb.
Prevents multi-store collapsing into one backing file.
- Generator manual check: stop assuming the project lands in CWD or
/tmp/throwaway; generate into an explicit mktemp dir via --dir.
- Removed references to a non-existent crates/edgezero-core/src/
hooks.rs — Hooks + ConfigStoreMetadata both live in app.rs.
* Tighten plan: four review findings before execution
- Macro compile-fail tests: Task 3.2 now adds `trybuild = "1"` to
edgezero-macros [dev-dependencies] explicitly (only `tempfile` was
there), with a tests/ui/*.rs fixture + .stderr golden per rejected
case.
- External-consumer test env guard: tests/lib_consumer.rs must restore
EDGEZERO_MANIFEST via an RAII EnvOverride guard and stay a single
#[test] (no in-binary parallelism); a shared Mutex guard is required
if more env-touching tests are ever added.
- WASM contract test commands pinned: Task 2.7 step 6 names the exact
target / features / runner per adapter (cloudflare wasm32-unknown-
unknown + wasm-bindgen; fastly wasm32-wasip1 + Viceroy; spin
wasm32-wasip1 + Wasmtime), deferring to test.yml as source of truth.
- app-demo e2e lifecycle: Task 8.1/8.2 now require an ephemeral port
(no hard-coded 8787), a readiness poll (no bare sleep), and RAII
teardown that kills the demo server even on assertion failure;
the loop is preferably a Rust integration test, not shell-in-YAML.
* Plan: wire new commands into the default binary, upgrade scaffold, align gate
- Default `edgezero` binary wiring (High): commits 4-7 now have explicit
steps to add Auth / Provision / Config(Validate|Push) to the default
edgezero-cli `Command` enum and `main.rs` dispatch (raw run_* — the
default binary has no app struct), with `edgezero --help` / parse
tests. Previously only the original five commands and app-demo-cli
were wired; the spec requires the new subcommands on the default
binary too. New Task 4.2 covers `config`; Task 5.2/6.1/7.2 extended.
- Generated `<name>-cli` template upgrade (Medium): new Task 8.2 updates
templates/cli/src/main.rs.hbs to the full eight-command set once
auth/provision/config exist, wiring the scaffold's config arm to the
typed functions with the generated project's config struct. Generator
test asserts it.
- Full-gate alignment (Medium): added a canonical "## The full gate"
section with the exact five CI commands from CLAUDE.md / the
workflows (cargo check uses --features "fastly cloudflare spin", not
--all-features). Every "run the full gate" step references it; fixed
the commit-1 and commit-8 gate steps and the Codebase-facts CI line
that had drifted to --all-features.
Commit-8 tasks renumbered (8.2 CI wiring -> 8.3; walkthrough/audit -> 8.4).
* Plan: fix three crate-dependency gaps for typed-config wiring
- app-demo-cli missing app-demo-core dep (High): Task 4.3 now adds
`app-demo-core = { path = "../app-demo-core" }` to
app-demo-cli/Cargo.toml — it references AppDemoConfig once typed
`config validate` / `config push` are wired, but its deps were only
edgezero-cli/clap/log.
- Generated <name>-cli template missing core-crate dep (High): Task 8.2
now also updates templates/cli/Cargo.toml.hbs to depend on
`{{name}}-core` (path dep), and the generator test asserts the
scaffold builds with that dependency and resolves the typed config
type.
- AppConfig macro + validator availability (Medium): chosen route
stated explicitly — `edgezero-core` re-exports the `AppConfig` derive
(matching the existing `action`/`app` re-exports), so a config crate
needs only `edgezero-core` for the macro, no direct edgezero-macros
dep. Task 3.4 updates templates/core/Cargo.toml.hbs to add
`validator` (with derive); Task 3.5 verifies app-demo-core already
carries edgezero-core + validator + serde. Generator test checks the
scaffolded core crate builds.
Task 3.4 / 4.3 / 8.2 steps renumbered to fit the inserted dependency
steps.
* Plan: generator context for config type name + validator workspace seed
- Generated config type placeholder (Medium): Task 3.4 step 1 now
explicitly adds a `NameUpperCamel` key to the generator Handlebars
context (derived from `name`: split on -/_, upper-case each segment,
join — `my-app` -> `MyApp`), with a unit test. Templates reference
`{{NameUpperCamel}}Config`; the key was previously unset (generator
data only had name/proj_core/proj_core_mod/proj_mod).
- validator workspace-dep plumbing (Medium/Low): Task 3.4 step 3 now
names the generator change explicitly — `templates/core/Cargo.toml.hbs`
uses `validator = { workspace = true }`, so `validator` must also be
added to the generator's workspace-dependency seed
(`seed_workspace_dependencies` in generator.rs), which omits it today.
- Duplicate Step 4 in Task 3.4 (Low): Task 3.4 renumbered cleanly to
Steps 1-6.
* Plan: fix generated-CLI import path + guarantee valid NameUpperCamel ident
- Generated CLI import (Medium): the cli template's `use` must
reference the core crate's Rust module name, not the package name.
`use {{name}}_core::...` renders `my-app_core` for `my-app` (invalid
Rust). Task 8.2 now uses `{{proj_core_mod}}` — the hyphen-to-
underscore module form the generator already exposes.
- NameUpperCamel validity (Medium/Low): Task 3.4 step 1 derivation now
guarantees a valid Rust type identifier — derive from the sanitized
crate name, drop empty segments (absorbs a leading `_`), and prefix
with `App` when the result would start with a non-letter (digit-
leading project names). Unit test covers `123-app` -> `App123App`,
`_foo` -> `Foo`, etc.
* Fixed formatting
* Address PR review: stale scaffold deps + KV pagination scan-cap bug
ChristianPavilonis review:
* Generator dependency seeds were stale relative to the adapters:
worker 0.7 → 0.8
fastly 0.11 → 0.12
simple_logger 4 → 5
Scaffolded projects pinned older provider SDKs than the adapters
expect, risking public-type mismatches in generated entrypoints.
* `PersistentKvStore::list_keys_page` lost keys when the scan cap was
hit. On a cap-hit the loop broke with `reached_end = false`, but the
cursor was only emitted when `live_keys.len() > limit`. An
under-filled page (cap reached while skipping a long expired run)
therefore returned `cursor: None` and callers stopped paginating,
silently missing live keys past the expired run. Now a cap-hit is
tracked and the last scanned key is returned as the resume cursor.
Added `list_keys_page_returns_resume_cursor_when_scan_cap_is_hit`.
`LIST_SCAN_BATCH_SIZE`/`MAX_SCAN_BATCHES` are lowered under
`cfg(test)` so the cap path is reachable with a 43-entry fixture
instead of 25k; pagination correctness is batch-size-independent.
* Commit 1: extensible edgezero-cli library + generator + app-demo-cli
Turn edgezero-cli into lib + bin so downstream projects can build their
own CLI binary reusing any subset of the built-in commands.
- Promote Command variant fields into standalone #[derive(clap::Args)]
structs (BuildArgs / DeployArgs / ServeArgs; NewArgs already standalone),
each #[non_exhaustive] + Default for external construction.
- Add src/lib.rs exposing the public API: run_build / run_deploy /
run_serve / run_new / run_demo, init_cli_logger, and the args module
(pub mod, not pub use — restriction lint). main.rs becomes a thin
wrapper over the library.
- Rename the `dev` subcommand to `demo` (dev is reserved for a future
dev-workflow command): dev_server.rs -> demo_server.rs, run_dev ->
run_demo (now Result<(), String>), Command::Dev -> Command::Demo.
- Extend the generator to scaffold a crates/<name>-cli crate from new
templates/cli/ Handlebars templates; seed clap + edgezero-cli as
workspace dependencies; add crates/<name>-cli to the workspace members.
- Add the handwritten examples/app-demo/crates/app-demo-cli crate as the
canonical downstream consumer, with a --help smoke test.
- Add crates/edgezero-cli/tests/lib_consumer.rs: external-consumer
integration test proving the public API is usable from outside.
- Docs: cli-reference.md (demo rename + "Building Your Own CLI"),
getting-started.md, CLAUDE.md.
All gates green: fmt, clippy -D warnings, cargo test --workspace,
feature cargo check, spin wasm32; app-demo workspace fmt/clippy/test.
* Make `demo` example-only; `serve --adapter axum` runs the axum adapter
After the dev->demo rename, `demo` should mean "run the bundled example",
not "run the project's axum adapter". Drop `try_run_manifest_axum` (and
its `load_manifest_optional` helper) from `demo_server`: `edgezero demo`
now always starts the built-in example server on 127.0.0.1:8787 and never
reads `edgezero.toml`.
`edgezero serve --adapter axum` is now the single, unambiguous way to run
a project's axum adapter (it runs `[adapters.axum.commands].serve`). This
removes the demo / serve --adapter axum behavioral overlap. Docs updated.
* Plan: mark Commit 1 done, fix stale branch/path, expand Fastly in Commit 2
- Commit 1 marked DONE (landed 1d582dd + follow-up 06f4b72) with a
Status section, so workers don't redo already-landed work.
- Working-branch reference corrected: feature/extensible-cli (was the
stale docs/extensible-cli-library-spec).
- app-demo edgezero-cli dep path fixed to ../../crates/edgezero-cli
(relative to the workspace manifest; the four-up path was wrong and
would break the demo workspace).
- Task 2.7 Fastly step expanded from one line to explicit per-kind
registry steps + contract tests: Fastly is Multi for KV/config/
secrets, two logical stores per kind, per-id name resolution,
id-keyed contract coverage under Viceroy — parity with the
cloudflare/spin acceptance criteria.
* Spec: document the namespaced args API (edgezero_cli::args::*)
Commit 1 shipped `pub mod args` rather than crate-root re-exports: a
root `pub use args::{...}` trips clippy::pub_use (the restriction group
is -D-denied workspace-wide). §4 now documents the supported API as
edgezero_cli::args::BuildArgs etc., with run_* staying at the crate
root, and updates every run_* signature to &args::<T>. Matches what
1d582dd actually exposes and what lib_consumer.rs / cli-reference.md
already use.
(Reviewer's second finding — demo overlapping serve --adapter axum in
1d582dd — was already resolved by 06f4b72; no action.)
* Address PR review: scaffold templates fail their own clippy/test gate
Review comment #7: a freshly generated project failed its own
restriction-deny clippy gate immediately.
- Core handler template: mirror app-demo's passing structure —
fallible `stream`/`IntoResponse` usage (no production `.expect`),
alphabetically ordered structs, grouped test items, `IntoResponse`
imported anonymously.
- Adapter host stubs: add `#[expect(clippy::print_stderr, reason)]`
and `allow(dead_code, reason)`; the axum entrypoint returns
`anyhow::Result` instead of `eprintln!` + `process::exit`.
- Inline the project-core `App` type in adapter entrypoints so import
order stays stable regardless of project name.
- key_value_store: replace `#[cfg(not(test))]` consts with
`if cfg!(test)` and rename a `cursor` binding that shadowed the
parameter (clippy `cfg_not_test` / `shadow_unrelated`).
- Add scaffold lint-coverage assertions to the generator test.
* demo: run app-demo via run_app for full manifest setup
`edgezero demo` now delegates to `edgezero_adapter_axum::dev_server::run_app`,
running the bundled app-demo example the same way its own axum adapter does.
This wires the complete manifest setup (routing, KV/config/secret stores,
logging, host/port) instead of a hand-rolled echo router.
The demo path requires the `dev-example` feature; without it `run_demo`
returns an actionable error.
* Plan/spec: rename "Commit N" to "Stage N"
The eight numbered work units are now "stages" rather than "commits" —
each stage may span multiple git commits. Literal git-commit actions
(commit steps, `git commit -m`, the PR head commit) keep the "commit"
wording.
* Formatting
* Make demo a contributor-only command; rename feature to demo-example
Addresses review findings on the demo subcommand:
- demo is exposed only when built with the new `demo-example` feature.
Generated CLIs and app-demo-cli no longer expose `Demo` at all — a
downstream project has no bundled app-demo to run. The default
`edgezero` binary gates `Command::Demo` on `demo-example`, so the
advertised `--help` surface matches what actually works.
- `demo-example` (renamed from `dev-example`) now also pulls in
`edgezero-adapter-axum`, making the feature self-contained.
- getting-started.md points generated projects at
`edgezero serve --adapter axum`; cli-reference.md documents `demo`
as contributor-only.
- NewArgs now derives Default and is #[non_exhaustive], matching the
other public *Args structs.
- Generated handler tests serialize API_BASE_URL access behind a
mutex + RAII env guard.
- Refreshed README, CLAUDE.md, architecture docs, and agent docs for
the dev->demo / dev-example->demo-example rename.
* Fix binary name, stale dev docs, and scaffold drift
Addresses review findings on the Stage 1 surface:
- Add a `[[bin]] name = "edgezero"` target so `cargo build` produces
`target/debug/edgezero` — the name every doc and the clap `about`
already use.
- Remove the inert `--local-core` flag from `NewArgs`; it was never
read by the generator.
- Warn when `edgezero new` falls back to a Git dependency for
`edgezero-cli`: the generated CLI crate needs `edgezero-cli` as a
published library, so an out-of-repo scaffold only builds once that
is available on the referenced remote. In-repo generation uses a
path dependency and is unaffected.
- Replace removed `edgezero dev` references with
`edgezero serve --adapter axum` in the root README, architecture,
and axum adapter docs.
- Drop `run_demo` from the "build your own CLI" surface (it is
contributor-only), and add the generated `*-cli` and Spin adapter
crates to the scaffold structure docs.
* Generate path dependencies to the local edgezero checkout
Fixes fresh `edgezero new` projects failing to build outside the repo.
The generated CLI crate imports `edgezero_cli`, but dependency
resolution fell back to a Git dependency whenever the output directory
was outside the repo root — and the published `edgezero-cli` has no
library target, so every `edgezero_cli::...` import failed.
- Locate the edgezero checkout via `CARGO_MANIFEST_DIR` (baked in at
build time) instead of the current directory, so generation finds the
checkout regardless of where the project is created or where the
command runs.
- When the output directory is outside the checkout, emit an absolute
path dependency rather than the Git fallback. The Git fallback now
only applies to a binary detached from its source tree.
- Assert in the generator test that the scaffold resolves edgezero
crates to path dependencies, so a regression to the Git fallback is
caught by `cargo test -p edgezero-cli`.
- Add an opt-in (`#[ignore]`) integration test that runs `cargo check`
on the generated CLI crate, proving it compiles against the local
`edgezero-cli` library.
- Drop the stale `--local-core` option from the CLI reference docs.
* Verify the full generated workspace compiles, not just the CLI crate
Broaden the opt-in scaffold test to `cargo check --workspace` and drop
`--offline`: a freshly generated project has no lockfile, so offline
resolution of transitive registry crates is unreliable (true of any
scaffolded project). Online, the full generated workspace compiles.
* Fix generated-README serve command; align plan/spec with 4-command CLI
- Adapter README `dev_steps` snippets advised `edgezero-cli serve
--adapter ...`, but the binary is `edgezero` (the `edgezero-cli`
package builds `target/debug/edgezero`). Corrected all four adapters
(axum, cloudflare, fastly, spin) so generated-project READMEs show a
working command.
- Updated the plan and spec acceptance notes: generated and app-demo
CLIs expose the four downstream built-ins (build/deploy/new/serve),
not five — `demo` is contributor-only and absent from downstream
CLIs. Also corrected the Stage 8 generated-CLI command count.
* Drop redundant cfg attributes in spin KV/secret store modules
The key_value_store and secret_store modules are already gated at their
mod declaration in lib.rs (#[cfg(all(feature = "spin", target_arch =
"wasm32"))]), so every per-item copy of that same cfg inside the files
was a tautology. Removing the 14 no-op attributes makes both files
consistent with their sibling request.rs/response.rs/proxy.rs, which
already rely on the gated mod declaration.
* Wire generated-project compile check into CI; fix stale plan lines
- Add a CI step that runs the `generated_project_builds` test
(`-- --ignored`), so the Stage 1 scaffold regression — a fresh
`edgezero new` project failing to compile — is caught by CI rather
than only by manual runs.
- Correct two stale Stage 1 plan steps: a default `cargo build
-p edgezero-cli` exposes four subcommands, not five; `demo` is gated
behind the `demo-example` feature.
* Fix generated wasm adapters and project-name sanitisation
Stage 1 review findings on generated projects:
- Cloudflare adapter template called `run_app(req, env, ctx)` but the
API takes `manifest_src` first — generated Cloudflare crates failed
to compile for wasm32. Aligned the template with the other three
adapters and the handwritten app-demo crate.
- The Spin `#[http_component]` macro expands to an unsafe wasm export,
which trips the generated workspace's `unsafe_code = "deny"` gate.
Added a narrow wasm-only `#[allow(unsafe_code)]` with a reason to the
Spin entrypoint, in the template and in app-demo.
- `sanitize_crate_name` mangled uppercase letters to `-`, so
`edgezero new MyApp` produced the invalid package name `-y-pp-core`.
It now lower-cases ASCII letters, keeps `-`/`_`, collapses other
characters, and trims leading/trailing separators; added unit tests.
- The opt-in `generated_project_builds` test only checked the host
target. It now also runs `cargo check` for each adapter's wasm
target (skipping a target that is not installed), which is where the
two failures above lived.
Plan: marked PR #253 merged, and recorded two post-review Stage 2
design inputs — downstream binaries must build without an
`edgezero.toml`, and the manifest holds only non-adapter-specific
config.
* Plan: clear stale PR #253 gating and five-built-ins references
- Status block no longer calls Stage 2 "gated on PR #253" — the
precondition is met (PR #253 merged).
- Task 8.2 now says Stage 1 created the generated CLI template with
four downstream built-ins, not five (demo is contributor-only).
* Spec/plan: revise Stage 2 to the portable-manifest + EDGEZERO__ design
Reworks spec §6.6/§8 and the plan's Stage 2 tasks for the design agreed
in review:
- edgezero.toml is portable and non-adapter-specific — [app], routes,
[environment], and [stores.<kind>] logical ids/default only. No
[adapters.*] table.
- The manifest is never compiled into the binary; the app! macro bakes
the portable config into the App/Hooks type at compile time, and
run_app::<A>() drops its manifest_src parameter (no include_str!).
- Adapter-specific runtime config — store platform names, tuning,
host/port, logging — comes from EDGEZERO__* environment variables at
runtime, with defaults when absent.
- An adapter binary builds and runs with no edgezero.toml and zero env
vars.
Plan Task 2.1–2.9 rewritten accordingly (adds the EDGEZERO__ env-config
layer task; drops the in-manifest per-adapter mapping).
* Stage 2 Task 2.1: portable manifest store schema
Rewrites the manifest store model to the §6.6 portable schema:
- `[stores.<kind>]` now carries only logical `ids` (non-empty) and an
optional `default` (required when >1 id, must be a declared id). The
five per-adapter store config types collapse into one reusable
`StoreDeclaration`.
- The pre-rewrite store schema (`[stores.<kind>] name`,
`[stores.config.defaults]`, `[stores.<kind>.adapters.*]`, `enabled`)
is a hard load error whose message points at the migration guide.
- Store helper methods resolve a store's name to its logical default
id (interim — `EDGEZERO__*` env overrides arrive in Task 2.2).
- `[stores.config.defaults]` and its axum dev-server seeding are gone.
- Migrated `examples/app-demo/edgezero.toml` and the generated
`edgezero.toml.hbs` template to the new schema.
Scoped to store types only; `[adapters.*]`, the env layer, and adapter
store registries are later Stage 2 tasks.
* Stage 2 Task 2.2: EDGEZERO__* environment-config layer
New `edgezero-core::env_config` module: parses `EDGEZERO__`-prefixed
environment variables (`__` = key-path separator, segments lower-cased)
into an `EnvConfig` value with accessors for store platform names +
tuning, bind host/port, and logging level.
- `from_env()` reads the process environment; `from_vars()` lets the
Cloudflare adapter supply its `Env` binding (no `std::env` there).
- `store_name(kind, id)` falls back to the logical id when unset.
Additive only — wired into the runtime in later Stage 2 tasks.
* Stage 2 Tasks 2.3 + 2.4: bake portable stores into Hooks; drop manifest_src from run_app
Couples the macro/runtime change with the adapter signature change so the
workspace and `examples/app-demo` stay buildable in a single commit.
- `Hooks::stores() -> StoresMetadata` replaces `config_store()`. The
`app!` macro emits portable `StoreMetadata { default, ids }` for
`[stores.config|kv|secrets]`. `Hooks::stores()` defaults to empty so
apps built without the macro still compile.
- `run_app::<A>()` no longer takes `manifest_src` on any adapter — axum,
cloudflare, fastly, spin. Each reads `A::stores()` and layers
`EDGEZERO__*` env config on top (logging level, bind host/port, store
platform names). All four entrypoint templates, all four
`app-demo-adapter-*` consumers, and `edgezero-cli/src/demo_server.rs`
drop `include_str!("edgezero.toml")`.
- axum `resolve_addr` reads `EDGEZERO__ADAPTER__HOST`/`PORT` only; the
`[adapters.axum.adapter]` fallback is gone (consistent with the §6.6
no-runtime-tables rule).
- cloudflare derives the exact `EDGEZERO__STORES__<KIND>__<ID>__NAME`
keys from baked metadata to query the worker `Env` (workers cannot
enumerate). Deprecated `run_app_with_manifest` is removed.
- spin drops `dispatch_with_manifest`; the KV label resolves from
`EDGEZERO__STORES__KV__<ID>__NAME` or the declared default id.
All five CI gates green + `examples/app-demo` tests pass.
Tasks 2.5–2.9 (async ConfigStore, store registries, extractors,
app-demo/templates/docs migration, ship gate) follow.
* Stage 2 Task 2.5: async ConfigStore, new KvError variants, store registry, id-keyed RequestContext
Lands the runtime-API shape for §6.6 multi-store support. No adapter
yet builds a `StoreRegistry` — that arrives in Task 2.6. The new
RequestContext accessors are wired with a legacy single-handle
fallback so all four adapters keep compiling and tests stay green
through the transition.
- `ConfigStore::get` → `async` (`#[async_trait(?Send)]`). Required for
Cloudflare's KV-backed config store (§8 Task 2.6) which is async at
the SDK boundary. All four adapter impls + `FixedConfigStore` test
doubles + app-demo's `MapConfigStore` / `UnavailableConfigStore` are
updated; the contract-test macro switches to `block_on` (same pattern
as the KV contract macro). `ConfigStoreHandle::get` follows.
- New `KvError` variants with `From<KvError> for EdgeError` mappings:
- `Unsupported { operation }` → `EdgeError::not_implemented` (501).
Used by Spin TTL writes (§6.7), where `key_value::Store::set` has
no expiry parameter.
- `LimitExceeded { message }` → `service_unavailable` (503). Used by
Spin's `get_keys` cap (`max_list_keys`, §6.7).
A new `EdgeError::NotImplemented` variant + constructor backs the
501 mapping.
- New `edgezero_core::store_registry` module:
- `StoreRegistry<H> { by_id: BTreeMap<String, H>, default_id: String }`
with `default()`, `default_id()`, `ids()`, `named()`, `new()`.
- `KvRegistry` / `ConfigRegistry` / `SecretRegistry` type aliases.
- `BoundKvStore` / `BoundConfigStore` / `BoundSecretStore` aliases for
the existing handle types so future call sites can speak in
registry terms without coupling to the legacy names.
- `RequestContext` gains id-keyed accessors `kv_store(id)` /
`config_store(id)` / `secret_store(id)` and the `_default()` helpers.
Each reads from the matching registry in extensions when present
(strict lookup — unknown ids yield `None`); otherwise falls back to
the legacy single-handle stash so today's adapters continue to work.
The pre-existing no-arg `config_store()` is renamed to
`config_handle()` to free the symmetric name; the few in-tree
call sites (axum service + four adapter contract tests + app-demo's
config handler) are updated. Handler migration to the id-keyed API
lands in Task 2.8.
Tests added: 4 RequestContext registry/fallback cases (kv, config,
secret), 2 KvError → EdgeError mappings (Unsupported → 501,
LimitExceeded → 503), 6 StoreRegistry unit tests. All five CI gates
green plus `examples/app-demo` tests.
* Stage 2 Task 2.6 (partial): axum store registries; Spin KV uses new error variants
First slice of Task 2.6. The remaining adapters (fastly, spin, cloudflare)
follow in subsequent commits; the legacy single-handle path stays live
in parallel so the workspace and `examples/app-demo` keep building
through the transition.
axum:
- `EdgeZeroAxumService` and `AxumDevServer` gain
`with_{kv,config,secret}_registry` setters alongside the existing
single-handle ones. Both can coexist; the registries are inserted
into request extensions in addition to the legacy handles, and
`RequestContext` prefers a registry when present.
- `dev_server::run_app` builds the three registries from `A::stores()`
+ `EDGEZERO__STORES__*`:
- KV: one `PersistentKvStore` per declared id at
`.edgezero/kv-<slug>-<hash>.redb` (file name derived from the
platform name from `EDGEZERO__STORES__KV__<ID>__NAME` or the id
default).
- Config: one empty `AxumConfigStore` per declared id; the
`.edgezero/local-config-<id>.json` read path lands when stage 7's
`config push` is wired.
- Secrets: axum is `Single` (§6.6) — every declared secrets id maps
to the same env-backed `EnvSecretStore`.
spin:
- `SpinKvStore::put_bytes_with_ttl` now returns
`KvError::Unsupported { operation: "put_bytes_with_ttl" }` (was
`KvError::Validation`) — semantically correct per §6.7 and matches
the variant added in Task 2.5.
- `SpinKvStore::list_keys_page` now returns
`KvError::LimitExceeded` (was `KvError::Validation`). Paginated
listing with an env-driven `max_list_keys` cap lands in the spin
registry-wiring commit.
All five CI gates green; `examples/app-demo` tests pass.
* Stage 2 Task 2.6 (partial): fastly store registries
Adds a registry-aware dispatch path (`dispatch_with_registries`) that
builds per-id `KvRegistry`/`ConfigRegistry`/`SecretRegistry` from
baked `Hooks::stores()` + `EDGEZERO__STORES__*`. `run_app` switches to
it; the legacy single-handle helpers (`run_app_with_config`,
`run_app_with_logging`, the individual `dispatch_with_*` entry points)
remain for back-compat.
Fastly is `Multi` for all three kinds (§6.6). Per kind:
- KV: each declared id opens its own `FastlyKvStore` via the platform
name from `EDGEZERO__STORES__KV__<ID>__NAME` (default = id). The
per-id open is required — declaring `[stores.kv]` means failure to
open is a runtime error, not silent degradation.
- Config: each declared id opens its own `FastlyConfigStore` via
`EDGEZERO__STORES__CONFIG__<ID>__NAME`; missing stores log a
one-time warning and the id is dropped from the registry.
- Secrets: `FastlySecretStore` is stateless (it opens the named store
per call), so one shared `SecretHandle` is registered under every
declared id for now. Per-id platform-name binding lands when Task
2.7 reshapes `BoundSecretStore` to capture the name.
All five CI gates green; `examples/app-demo` tests pass.
* Stage 2 Task 2.6 (partial): spin store registries + paginated list_keys_page
Wires Spin to the registry model and replaces the unconditional
listing error with real client-side paging.
Per §6.6 / §6.7 Spin capability map:
- KV (Multi): each declared id opens its own `SpinKvStore` under the
Spin label resolved from `EDGEZERO__STORES__KV__<ID>__NAME`. The
`max_list_keys` paging cap is per-id, read from
`EDGEZERO__STORES__KV__<ID>__MAX_LIST_KEYS` and defaulting to 1000.
Required: a `[stores.kv]` id failing to open is a runtime error.
- Config (Single): the one shared `SpinConfigStore` is registered
under every declared id. Capability validation that catches
`ids.len() > 1` on Spin lands in `config validate` (§10).
- Secrets (Single): the one shared `SpinSecretStore` is registered
under every declared id.
`SpinKvStore::list_keys_page` materialises `Store::get_keys()`, filters
by prefix, sorts, applies the `max_list_keys` cap, and slices the page
via the new pure `crate::kv_pagination::paginate_keys` helper. Pagination
invariants are unit-tested on the host (8 tests covering: empty prefix,
prefix filter + sort, page-smaller-than-match cursor, cursor advance,
final page, cap exceeded → `LimitExceeded`, cap=0 disables, cursor past
end). The wasm-only `SpinKvStore` is the production consumer.
`run_app` switches from the legacy `SpinStoreSettings` settings path to
`request::dispatch_with_registries`. The now-dead
`SpinStoreSettings` + `resolve_store_settings` +
`dispatch_with_store_settings` slice is removed; the old single-handle
tests in `lib.rs` are superseded by the upcoming registry-aware
contract tests (Task 2.6 final).
All five CI gates green; `examples/app-demo` tests pass.
* Stage 2 Task 2.6 (partial): cloudflare store registries; CloudflareConfigStore [vars] → KV async
Replaces the pre-rewrite JSON-string config store with a Cloudflare KV
namespace backing (§6.6) and wires per-id registries through dispatch.
CloudflareConfigStore rewrite:
- Old: `[vars]` string binding whose value is a JSON object;
parse-and-cache at construction with O(1) map reads.
- New: a Cloudflare KV namespace binding opened at construction
(`env.kv(binding_name)`); `get(key)` reads asynchronously…
aram356 added a commit that referenced this pull request Aug 2, 2026
Spec-alignment (findings #1, #6, #7): synthesised manifests now match
the spec's normative baselines exactly, verified by per-adapter
exact-content tests.
- Cloudflare drops the `[build]` table; Fastly drops the empty
`authors` array; Spin drops the `[component.<id>.build]` table.
- Spin `allowed_outbound_hosts` becomes opt-in via
`[adapters.spin.adapter].allowed_outbound_hosts`, defaulting to
Spin's deny-all baseline. Both `edgezero new` and clean-clone read
the same manifest, so the emitted file stays byte-identical.
- Fastly cloud `provision` no longer auto-captures `service_id` from
the gitignored, per-machine `fastly.toml`. Per the v1 contract the
writeback is a documented one-time manual copy after `fastly compute
deploy`; auto-capturing it let a stale local file overwrite the
team's committed id.
Correctness (findings #2, #3, #4, #8):
- Cloudflare cloud `provision` reconciles namespace ids against the
tracked `[adapters.cloudflare.deployed]` block: a local id that
disagrees aborts with a conflict error instead of silently
overwriting the committed one, and a fresh clone whose wrangler.toml
lacks the id has it restored rather than creating a duplicate remote
namespace.
- Fastly typed-secret provisioning seeds the Viceroy
`[[local_server.secret_stores.<name>]]` table under the resolved
PLATFORM name (`TypedSecretEntry.platform`), the name the runtime
opens, instead of the raw logical id -- an env override no longer
points the runtime at a store the seed never created.
- Symlink guards extended to the remaining provision/push write sites:
`write_baseline_to_disk`, Axum's local-config JSON, and Spin's local
KV SQLite db. `env_file::reject_symlinked_target` is now the shared
final-component check.
- Deployed-field ownership is enforced in core `Manifest` validation
(canonical adapter map), so reduced-feature builds and non-CLI
readers reject a field placed under an adapter that does not own it,
not just the CLI's registry-based check.
Also removes planning/review provenance markers (PR-round, Task, Stage,
Phase, review-priority) from code comments, and corrects the CHANGELOG
lock-path reference.
aram356 added a commit that referenced this pull request Aug 19, 2026
…ects
Two P0 blockers (both live-verified):
- Spin build no longer TRUNCATES its own artifact. `refresh_declared_source`
compared lexical paths, so the default `source` (`../../target/.../x.wasm`,
a relative path with `..`) never matched the normalised artifact and
`fs::copy` copied the file onto itself -> 0 bytes. Now guarded by a
canonicalize-based same-file check (also for the conventional refresh), plus
containment (refuse a `source` resolving outside the workspace or through a
symlink) and single-component matching (#1, #5).
- Fresh Cloudflare scaffolds can build/serve/deploy. The synthesised
`wrangler.toml` now declares `[build] command = "worker-build --release"`
(main points at the worker-build-generated shim), and the adapter drops its
`[commands]` shell block so build/serve/deploy route through the registry
dispatch, which passes `--config <manifest>` (also fixes custom manifest
paths being ignored) (#2, #7).
Spin:
- `[adapters.spin.adapter].cloud = true` is the SOLE Fermyon Cloud selector;
the legacy deploy-command heuristic and its `manifest_adapter_deploy_cmd`
field are removed, and `config diff` threads the flag like `config push` (#3).
- `edgezero serve` forwards parent-shell `EDGEZERO__*` overrides to the guest
via `spin up --env` (highest precedence) so a shell override reaches the
sandboxed component (#4).
- deploy uses `spin cloud deploy` (bare `spin deploy` needs a plugin on
Spin 3+/4) (#6).
Cloudflare / Axum (via subagents):
- typed secret keys are validated for `.dev.vars` round-trip (empty/=/#/
newline/whitespace) (#16); local push refuses a `.wrangler` symlink escape
(#11); axum runtime honours `EDGEZERO_MANIFEST` when locating `.edgezero`
(#8).
Fastly:
- cloud provision now writes the per-store `__NAME` runtime mappings into
`edgezero_runtime_env` so a renamed cloud store is reachable (#10); the
dry-run preflight redacts malformed-manifest parse errors and models the
real writer's inline-toml rejection (#13).
Convergence (via subagent): reprovisioning converges managed `__NAME`
overlays across env_file + all four adapters via a new
`upsert_lines_with_header` (#9).
CLI:
- dry-run staging refuses a TMPDIR inside the project (self-recursive copy)
and no longer follows a symlinked live path into the diff output (#12);
- cloud provision preflights manifest writability before mutating the account,
so an unwritable manifest can't orphan a created resource (#15).
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.

1 participant

@aram356