Repository navigation
Conversation
The June-07 review caught `npx prettier --check .` failing on `docs/guide/cli-walkthrough.md` lines 190 / 202-204. Prettier doesn't preserve indentation on continuation lines inside backtick-wrapped inline-code spans (`KEY=VALUE`, `SET <key> <value>`, `azure_cosmos`), so the de-indent it wants is purely a source-format change — the rendered VitePress output is unchanged because the surrounding backticks group the whole multi-line span as one inline code block. Pure formatting; no content edits. Verified: `(cd docs && npx prettier --check .)` is clean.
Adds the EdgeZero outbound HTTP design spec under docs/superpowers/specs/2026-05-21-outbound-http-design.md. Targets the PR #269 (feature/extensible-cli) baseline. The spec covers six requirements: - portable OutboundHttpClient trait with single send + concurrent send_all on every adapter (Axum, Cloudflare, Fastly, Spin) - per-request and shared deadline / timeout primitives with a documented dispatch budget and bounded-cooperative semantics on Fastly - bounded buffering with explicit persistent vs transient memory accounting and pre-append cap checks - manifest-driven [capabilities] declaration (nine capabilities total) with pre-dispatch enforcement gates at five CLI entry points - adapter contract test plan in three tiers (core mock, per-adapter translation, runtime) - four canonical URI accessors (backend_target, host_authority, sni_hostname, cert_host) so adapters share one canonical host/port/SNI/cert split Revised through 49 review rounds; non-normative resolution journal lives in Appendices A through AX.
Removes references to the originally-named driving consumer and the specific external protocol used as motivation: - consumer → "the driving pattern" / generic - Prebid-style → "fan-out-style" - OpenRTB → "the external batch protocol" - bidder → "target" - auction → "fan-out batch" - tmax → "batch deadline" The technical motivation (N concurrent outbound requests under a shared wall-clock deadline, results harvested in input order, small response bodies, homogeneous-budget common case) is preserved; only the named consumer and its protocol are scrubbed so the spec reads as a portable substrate rather than a single-consumer design. Section §3.3.2 retitled "Mapping an external batch deadline to EdgeZero deadlines"; status header gains a "Driving pattern" line in place of the old "Driving consumer" pointer.
Spec previously claimed Fastly behaviour that did not match the actual SDK / public API. This commit corrects the four normative claims, adds an app-facing consuming body accessor, and records the corrections in Appendix AY. Findings addressed: - lazy-streamed-response-passthrough downgraded Native -> BestEffort on Fastly. `Response::with_streaming_body` does not exist; `Response::stream_to_client()` is the actual API and is documented as incompatible with `#[fastly::main]`. Default scaffold falls back to buffered passthrough; lazy passthrough requires the non-main entry-point template tracked in new section 8 risk 12. - NameInUse semantics rewritten. Fastly's session-uniqueness rule is unconditional; the previous "identical name + identical properties is a re-registration that returns Ok" carve-out was false. SDK's `Backend::from_str(name)` returns a handle only and exposes no registered properties, so a NameInUse on a name not in this adapter's collision map is now an explicit fail-closed internal error rather than a silent property-trust fallback. - between_bytes_timeout is receive-side only per Fastly's Backend API docs. The previous claim that it bounded guest-to-origin writes is removed; the streamed-upload host-write phase is downgraded to BestEffort with the cooperative inter-chunk check as the only adapter-side bound. - Streamed-upload response overshoot tightened from per-chunk accumulator to closed-form bound: first_byte_ms (headers wait) plus one between_bytes_timeout (worst-case first-body-chunk read), one-shot. Footnote 1 single-send section + section 5.4 test row updated. - OutboundResponse::into_body() added as the app-facing consuming accessor for streamed-response orchestration. The send_all rustdoc recommends single send + futures::join_all + into_body() on Axum/CF/Spin as the canonical path; into_parts(..) stays adapter-facing. Appendix AY records the five resolutions. Status header bumped to "rounds 1-50, Date: 2026-06-08"; superseded-AR pointer extended to include AY.
Round 50 — Fastly SDK correctness pass (commit
|
Five round-50 carry-over findings: - Early section 4.3 dynamic-backend prose still preserved the stale identical-properties-re-register carve-out, contradicting the corrected step-5 algorithm. Rewritten in place to match the unconditional session-uniqueness contract. Two historical appendix entries (round-37 in Appendix AK) marked superseded by Appendix AY. - Fastly buffered-fallback for lazy passthrough named max_response_bytes as the cap, but that per-request cap is unavailable at response-converter time. Added FASTLY_RESPONSE_STREAM_BUFFER_BYTES adapter-level constant (mirrors AXUM_RESPONSE_STREAM_BUFFER_BYTES). Three section 5.4 rows rebucketed so Fastly is no longer in the CF/Spin lazy group; new Axum-and-Fastly buffered-fallback row carries both adapter constants. - Residual between_bytes_timeout write-side claim removed from the remaining section 5.4 stalled-upload mechanics row and from section 8 risk 7. Fastly write phase is BestEffort uniformly now; the public Backend API docs are cited as the source. - Spin host-write race rewritten against actual WASI output-stream semantics. Old wording said each write() is raced against a timer; WASI write() is nonblocking and readiness-polled, so the implementable pattern is subscribe-pollable + futures::select! vs timer + nonblocking check_write() + write() within the permitted byte count. - Typo: "docsare migrated" -> "docs are migrated" in section 1.3 non-goals. Status header bumped to rounds 1-51; AR-superseded pointer extended to include AZ.
Round 51 — round-50 carry-overs + Spin WASI write mechanics (commit
|
# Conflicts: # .github/workflows/test.yml # .tool-versions # Cargo.lock # Cargo.toml # crates/edgezero-adapter-axum/src/cli.rs # crates/edgezero-adapter-axum/src/config_store.rs # crates/edgezero-adapter-axum/src/dev_server.rs # crates/edgezero-adapter-axum/src/service.rs # crates/edgezero-adapter-cloudflare/src/cli.rs # crates/edgezero-adapter-cloudflare/src/lib.rs # crates/edgezero-adapter-cloudflare/src/request.rs # crates/edgezero-adapter-cloudflare/src/templates/src/lib.rs.hbs # crates/edgezero-adapter-fastly/Cargo.toml # crates/edgezero-adapter-fastly/src/cli.rs # crates/edgezero-adapter-fastly/src/config_store.rs # crates/edgezero-adapter-fastly/src/lib.rs # crates/edgezero-adapter-fastly/src/request.rs # crates/edgezero-adapter-spin/src/cli.rs # crates/edgezero-adapter-spin/src/cli/push_cloud.rs # crates/edgezero-adapter-spin/src/request.rs # crates/edgezero-adapter-spin/src/templates/runtime-config.toml.hbs # crates/edgezero-adapter-spin/src/templates/spin.toml.hbs # crates/edgezero-adapter-spin/tests/contract.rs # crates/edgezero-adapter/src/registry.rs # crates/edgezero-cli/Cargo.toml # crates/edgezero-cli/src/adapter.rs # crates/edgezero-cli/src/args.rs # crates/edgezero-cli/src/config.rs # crates/edgezero-cli/src/generator.rs # crates/edgezero-cli/src/lib.rs # crates/edgezero-cli/src/main.rs # crates/edgezero-cli/src/provision.rs # crates/edgezero-cli/src/scaffold.rs # crates/edgezero-cli/src/templates/app/name.toml.hbs # crates/edgezero-cli/src/templates/cli/src/main.rs.hbs # crates/edgezero-cli/src/templates/core/Cargo.toml.hbs # crates/edgezero-cli/src/templates/core/src/config.rs.hbs # crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs # crates/edgezero-cli/src/templates/root/Cargo.toml.hbs # crates/edgezero-cli/src/templates/root/README.md.hbs # crates/edgezero-cli/src/templates/root/edgezero.toml.hbs # crates/edgezero-cli/src/templates/root/gitignore.hbs # crates/edgezero-core/src/app.rs # crates/edgezero-core/src/app_config.rs # crates/edgezero-core/src/context.rs # crates/edgezero-core/src/env_config.rs # crates/edgezero-core/src/error.rs # crates/edgezero-core/src/extractor.rs # crates/edgezero-core/src/key_value_store.rs # crates/edgezero-core/src/lib.rs # crates/edgezero-core/src/manifest.rs # crates/edgezero-core/src/store_registry.rs # crates/edgezero-macros/src/app_config.rs # crates/edgezero-macros/tests/app_config_derive.rs # crates/edgezero-macros/tests/ui/secret_bogus_kind.stderr # crates/edgezero-macros/tests/ui/secret_with_serde_flatten.stderr # crates/edgezero-macros/tests/ui/secret_with_serde_skip_serializing_if.stderr # docs/.vitepress/config.mts # docs/guide/kv.md # docs/superpowers/plans/2026-05-20-cli-extensions.md # docs/superpowers/plans/2026-06-01-spin-kv-backed-config.md # docs/superpowers/plans/2026-06-04-spin-per-backend-push.md # docs/superpowers/specs/2026-06-01-spin-kv-config.md # examples/app-demo/Cargo.lock # examples/app-demo/app-demo.toml # examples/app-demo/crates/app-demo-adapter-fastly/fastly.toml # examples/app-demo/crates/app-demo-adapter-spin/spin.toml # examples/app-demo/crates/app-demo-cli/Cargo.toml # examples/app-demo/crates/app-demo-cli/src/main.rs # examples/app-demo/crates/app-demo-cli/tests/config_flow.rs # examples/app-demo/crates/app-demo-core/src/config.rs # examples/app-demo/crates/app-demo-core/src/handlers.rs # examples/app-demo/edgezero.toml # scripts/smoke_test_config.sh
- Define config-store / kv-store / secret-store capability semantics (bare enum variants had no doc comment, unlike the six outbound caps). - Add explicit transport-error mapping bullets to the Cloudflare (§4.2) and Spin (§4.4) adapters, matching Axum/Fastly (DNS/TLS/connect -> bad_gateway). - Collapse the now-obsolete dual-baseline enforcement-gate hedging: PR #269 has merged to main, so the #269 sibling-gate topology is the active shape and the pre-#269 wording is marked historical.
- §4.3: document Fastly send_all cancellation/drop semantics (no async cancellation; every PendingRequest is harvested; sibling deadline never aborts other slots). - §3.3.1 / §3.1.3: spell out Deadline::after overflow fallback and into_response()'s only Err (adapter-invariant internal) condition. - §5.4: add test rows for Json/ValidatedJson cap migration, the *Within explicit-cap extractors, the per-adapter capability() matrix, and back-compat manifest parse. - §5.5: state where Tier 2 runs per adapter (host gate for Axum; per-adapter wasm-target matrix for the three WASM adapters). - §7: add dispositions for adapter request.rs, the Tier 3 MockServer, the run_* gate-site files + ensure_capabilities home, and CLAUDE.md/workflow wasip2 refresh; note sha2 is already a workspace dep. - Collapse the now-obsolete dual-baseline (#269 pre/post) framing in §3.5.2, §5.5, and §6 to the post-#269 shape, keeping the pre-#269 note as history.
- into_response(): drop the unreachable 'body handle surrendered by a prior terminal call' Err example — all terminal methods take self by move, so double-consumption is a compile error; reword as infallible-in-safe-use with a reserved internal path. - §5.4 capability() matrix row: include the omitted BoundedCooperative variant (CapabilitySupport has four variants: Native / BoundedCooperative / BestEffort / Unsupported).
…er (WIP) Amends the spec from three source-verified investigations (spin-sdk 6/wasip3, worker 0.8.3/workerd, Spin host-sync lifecycle) plus review findings: - Spin: replace the unimplementable WASI-0.2 upload loop (wasi:io is deleted in WASI 0.3) with a hand-built wasi:http request; the SDK's high-level send spawns an uncancellable body pump. Lazy response passthrough = BestEffort (EdgeZero's own FullBody alias, not a platform limit). - Cloudflare: set-cookie IS preservable (compat_date already enables it); the collapse is HeaderMap::insert. Second collapse bug on the client-facing path (Headers::set) and a to_str().unwrap() panic hazard. Comma-joining of repeated non-set-cookie headers is the real, narrow loss. - Spin host sync: provision writes, build/serve/deploy validate at adapter.rs::execute (a hook in SpinCliAdapter::execute would be dead code). - Fastly SendErrorCause -> 504/502; RequestContext::new preserved; test seams behind test-utils (not cfg(test)); tiers redefined by owning crate. Known-open: a follow-up review found compile-level errors in the normative pseudocode (pub(crate) validator across crates, builder/getter name collisions, Spin pump result handling, StoredError reconstruction, Hooks defaults vs denied missing_trait_methods). Tracked in the decisions register; NOT ready for task-level planning.
…cs/outbound-http-spec
…ted)
Built a 2-crate compile-check skeleton (core + separate adapter crate) and let
the compiler/clippy adjudicate the review's compile-class findings:
- validate_for_dispatch: pub (pub(crate) is unreachable from adapter crates) [E0603]
- deadline accessors: single budget_inputs() struct accessor (getter names
collided with the public builder setters -> E0592 duplicate definitions)
- Spin pump: unwrap Result<Bytes,EdgeError>, keep+prioritize pump result
(join!(...).1 silently discarded source errors and cap overflow)
- StoredError: variant-specific snapshot enum ({kind,message} doubled Internal's
prefix and couldn't hold ConfigOutOfDate/MethodNotAllowed/NotFound payloads)
- Capability schema: inline in manifest.rs + derive Serialize (reconciled active
section with §7; separate capability.rs won't compile in the macro include)
- Baked manifest: app! must emit BOTH Hooks methods (clippy::missing_trait_methods
is denied -> proven error); manifest() uses a per-impl function-local OnceLock
calling a PUBLIC Manifest::from_baked_json (reparse must call pub(crate) finalize,
unreachable from the downstream generated impl)
- Spin host default: Option<Vec<String>> (absent=None https-only vs explicit
["*"]=http+https; bare Vec can't carry the distinction -- serde test passes)
- Fastly cache: MUST be session-scoped thread_local!, not a per-request client
field (client is constructed per inbound request; names are session-global ->
request #2 fails closed on NameInUse)
- provision: corrected -- receives NO host data; needs manifest re-read or a
signature change (my earlier 'no trait change' was wrong)
- config validate: run_config_validate_typed is a separate public entry generated
CLIs call directly; gate must be a shared inner op behind both entries
- Fastly test seam: recording BackendBuilder, not a cache inspector (Backend is
opaque -- getters don't round-trip SSL/SNI/cert/override_host)
Skeleton at scratchpad/skel compiles green in corrected form. Still open: policy
calls (loopback-vs-no-network, command-class gating, compression, migration
completeness incl PROXY_HEADER + error-JSON status field).
Skeleton (scratchpad/skel, compiles green) confirmed the CLI-gate wiring shape:
a gate on only one of run_config_validate / run_config_validate_typed leaves the
typed path silently ungated -> must be a shared inner gated op both entries call.
Policy decisions (all user-locked):
- Command-class gating: gate runtime-producing/mutating (build/serve/deploy/
provision/config push) + validation (config validate); EXEMPT read-only
diagnostic (config diff, auth status) and credential (auth login/logout). This
REVERSES the earlier 'gate config diff' lock. Gate count 6 -> 5; execute(..)
branches to skip auth. Swept §3.5.3 table, §5.4 enumerations (+ exemption
regression-guard row), §6/§7 counts, §1 header.
- Loopback tests: amend CLAUDE.md so 'network' = public internet; a loopback
mock origin is permitted (in-process fake rejected -- can't prove real socket
concurrency).
- Compression: decode single gzip/br; identity/unknown/stacked/repeated ->
passthrough untouched (bytes + content-encoding intact), never a hard fail.
Case-insensitive token match. Normative table added to §3.4.1.
- PROXY_HEADER (x-edgezero-proxy): PRESERVED through the rename.
Migration honesty (finding 13): replaced the false 'no public capability lost'
claim with an enumerated removal table -- ProxyHandle::client(), body_mut(),
extensions()/extensions_mut(), request_mut() are DROPPED (breaking); PROXY_HEADER
preserved. EdgeError JSON shape corrected to the REAL converter's
{status, kind, message, field_path?}, not {kind, message}.
…e appendices) Independent source-grounded verification review: compile-class CLEAN (no blockers), current-tree claims ALL accurate. Two active defects were my own incomplete command-class-gating sweep from the prior round, plus three stale appendix entries: - §7 CLI gate enumerations (both) still gated the EXEMPT config diff and said execute(..) covers auth. Corrected: execute gates build/serve/deploy only (branches to skip Auth*); four siblings run_provision/run_config_push_typed/ run_config_validate/run_demo; run_config_diff_typed is NOT a gate site. - §5.4 row asserted the execute gate fires on auth -> corrected to build/serve/ deploy only, + asserts config diff / auth do NOT hard-fail. - Appendix entries: added [SUPERSEDED] markers (not rewriting frozen history, matching the spec's existing 'superseded by AY' convention) to: lazy=Native- on-three-WASM-adapters (only CF Native now), the WASI-0.2 subscribe/check_write protocol (doesn't exist in SDK 6), and the #[cfg(test)] seam (must be test-utils). Also compile-verified the Fastly session-cache thread_local! pattern in the skeleton (request #2 reuses across per-request clients; mismatch fails closed). Gate count now consistently FIVE across §3.5.3, §5.4, §6, §7; auth + config diff exempt everywhere in the authoritative body.
…tives)
First executable TDD slice of the outbound-http implementation. Scoped to the two
purely-additive, skeleton-verified core primitives that keep cargo test --workspace
green at every step:
- EdgeError::BadGateway (502) / GatewayTimeout (504): variants, constructors, all
five exhaustive-match updates (kind_str/message/status/inner/into_response
field_path), + tests for status/kind/message and the {error:{status,kind,message}}
JSON shape (no field_path). Test code verified against the real error.rs APIs
(sync into_bytes(), nested error-wrapping).
- time.rs: Deadline (after with DEADLINE_FAR_FUTURE clamp + saturating checked_add,
at_instant/instant/remaining/is_expired), DispatchBudget struct, and the three
budget constants with exact verbatim values.
dispatch_budget() itself is explicitly deferred to Phase 1b (needs OutboundRequest +
the budget_inputs() accessor), which is the breaking proxy->outbound rename slice.
PLAN (all findings verified against the real tree before fixing): - Task 1 would NOT have compiled: enumerated only 5 exhaustive EdgeError matches, but there are EIGHT -- the 3 test-module matches (error.rs 281/335/369) have explicit panic-arms listing every other variant with NO '_' wildcard, so adding two variants breaks them. #[non_exhaustive] does not relax exhaustiveness inside the defining crate. Listed all 8 + added a compiler-driven catch step (E0004). - is_expired() was WRONG at exact equality: checked_duration_since returns Some(ZERO) at t==deadline, so it reported not-expired, contradicting the spec's 'deadline <= now => expired'. Now compares instants directly; remaining() returns None at equality. Added an equality test. - 'cargo test -p X a b' is invalid (verified: unexpected argument 'b'). Single filter. - GatewayTimeout's JSON contract was untested -> both variants table-driven, and the existing kind_strings_per_variant matrix (error.rs:502) gains rows (with the suffixed 502_u16/504_u16 literals the macro actually uses). - Flaky timing tests replaced with instant-bounded ones (no now()-1s underflow, no 50-60s tolerance); the Duration::MAX test now actually proves the 7-day clamp. - DispatchBudget moved to Phase 1b to ship with its producer (spec treats §3.3.2 carrier+producer as one contract). - fmt added to Task 1; Task 3 now runs all five CI gates incl. fmt --check and the wasm32-wasip2 target. - Phase 1b noted as multiple slices, not one atomic step. SPEC (stale pseudocode/footnotes the prose sweeps missed): - Fastly cache ownership normalized to ONE owner: session-scoped thread_local! + RefCell (single-threaded guest, no Mutex). Removed the contradictory per-client 'Mutex<HashMap> on FastlyOutboundClient, one per request context' claims, which fail on the 2nd inbound request (names are session-global, client is per-request). Test seam targets the thread-local, not a client field. - Footnote 2 claimed between_bytes_timeout bounds streamed-upload inter-chunk gaps; it is receive-side only and bounds nothing on the guest->origin write path. Now agrees with §4.3 and §8 risk 7: BOTH source-pull and host-write are BestEffort. - CLI gate pseudocode rewritten: execute(..) branches to skip Auth*, gates the TYPED push path (bundled run_config_push is an erroring stub), validate is adapter-less and loops configured adapters behind ONE shared gated_validate that both the bundled and typed entries call, demo reads the baked Hooks::manifest(), and run_config_diff_typed is explicitly NOT a gate site. - Spin sample applied '?' to Option<Duration> in a Result fn -> explicit let-else to gateway_timeout.
…r stages
Verified every finding against the tree/compiler before fixing.
PROVEN BUGS (skeleton-adjudicated):
- Trait-default static is NOT per-impl. Proof test: two Hooks impls relying on a
caching default BOTH returned the first impl's value (AppA=1, AppB=1) -- items
in generic fns are not monomorphized, so ONE static is shared by all
implementors. As specced, app A's baked manifest would serve app B's capability
checks. Fix: default manifest() returns None; the OnceLock lives in each
macro-generated impl (distinct static per impl). Test kept as a regression guard.
- The repo DENIES clippy::restriction wholesale (root Cargo.toml) and does NOT
allow-list missing_inline_in_public_items / min_ident_chars /
arithmetic_side_effects / expect_used / as_conversions. My Phase 1a Deadline
code and the spec's pseudocode both violated it:
* plan: added #[inline] to every public fn, d -> duration, replaced
with checked_duration_since + explicit zero filter via pure remaining_at(now)/
is_expired_at(now) helpers (which also make the tests exact/deterministic --
no tolerance windows, no now()-1s underflow; the clamp test now bounds
d.instant() to prove the 7-day clamp).
* clippy.toml allows expect/unwrap/panic IN TESTS but NOT arithmetic_side_effects
-- documented, hence checked_add(..).expect(..) in test code.
* spec: dispatch_budget .expect(..) -> explicit invariant EdgeError::internal;
fastly_timeout_ms casts + unchecked arith -> as_millis + saturating +
u64::try_from (compile-verified AND behaviour-tested: sub-ms->1, 1.5ms->2,
Duration::MAX clamps without panic).
FASTLY CONTRADICTIONS:
- Step 6 listed DNS/TLS as Backend::builder().finish() errors; they only arrive from
send. Split the two stages (BackendCreationError vs SendErrorCause), which also
makes the §5.4 rows writable (a fake builder cannot produce a DNS branch).
- Decided the ambiguous cause: DnsTimeout -> 504 (classify by 'did a timer fire?',
not by subsystem); DNS resolution failure (NXDOMAIN) -> 502.
- Canonical-accessor test row pointed at map inspection; Backend is opaque -> now
targets the recording BackendBuilder (map inspection reserved for identity/reuse).
- Purged mutex-era reasoning from the thread_local algorithm ('drop the lock' ->
'release the borrow'); removed the impossible 'prior session / another deployment'
NameInUse cause (names are session-scoped and may overlap across sessions) and
replaced it with the three real causes.
- Upload summary still claimed a BoundedCooperative write-side bound and that only
source-yield was the 'worst phase' -- both phases are unbounded BestEffort.
- [SUPERSEDED] markers on the two appendix Mutex-era entries.
STALE FOLLOW-UPS (verified already done -- would have scheduled redundant work):
- Risk 10 (Spin wasip2 refresh) CLOSED: CLAUDE.md already quotes wasip2 for gate 5
and lists Spin as wasm32-wasip2; surviving wasip1 refs are Fastly's (correct).
- §1 header no longer claims the CLI gates are unreconciled -- §3.5.3 reconciles them.
- §7 CLAUDE.md entry now carries the real deliverable: amend the unqualified
no-network rule to permit a loopback mock origin.
…rror policy Reviewer compiled the Phase 1a code under the repo's lint policy and found duration_suboptimal_units. I reproduced the repo's EXACT lint table in an isolated crate and found MORE than reported -- then iterated to green. PHASE 1a PLAN (now verified GREEN under the repo's real gate): - duration_suboptimal_units (pedantic; CI runs -D warnings so it FAILS the build): from_secs(7*24*60*60) -> from_hours(168); from_secs(60) -> from_mins(1). - arbitrary_source_item_ordering (denied restriction) -- NOT in the review, found by compiling: items must be ALPHABETICAL, and it policies ENUM VARIANTS too (proven: appending BadGateway after Validation errors). The plan said "add after Validation", which would have failed on first build. Corrected: BadGateway before BadRequest, GatewayTimeout between ConfigOutOfDate and Internal; constructors likewise. Deadline's methods reordered alphabetically. - Confirmed std_instead_of_core/std_instead_of_alloc ARE allow-listed, so `use std::time::Duration` is fine (checked the full allow-list rather than guessing). - Fixed the two "compares instants directly" descriptions -- impl uses checked_duration_since(..).filter(..). SPEC (High): - ManifestCapabilities/ManifestOutboundCapability derived only Deserialize while Manifest derives Serialize -> would break Manifest's derive and the macro crate. Both now derive Serialize. - SECURITY: hosts was Vec<String> defaulting to ["*"], but the renderer defines "*" as http+https while an ABSENT field must stay https-only. Every existing manifest that never declared hosts would silently gain cleartext outbound on next build. Now Option<Vec<String>>: None -> ["https://*:*"]; Some(["*"]) -> http+https (opt-in); Some([]) -> rejected. Validator signature updated to the inner slice. - Fastly error policy: replaced the two catch-alls with an EXHAUSTIVE per-variant table built from the real fastly-0.12.1 enums. Key correction: ConnectTimeoutTooLarge /FirstByteTimeoutTooLarge/BetweenBytesTimeoutTooLarge/NameTooLong/EncodingError and HttpRequestUriInvalid/InternalError mean EDGEZERO violated its own clamp/name/URI invariants -> `internal` (500), NOT bad_gateway. Mapping them to 502 disguises an adapter bug as an upstream failure. Widened the §5.4 "internal only on three paths" assertion to five, with a regression guard against the 502 mapping returning.
…t sweep)
Addresses ALL remaining feedback. Verified each against the real SDK/tree.
SECURITY -- baked-manifest gate failed OPEN:
- parse-failure and "no manifest" both collapsed to None, and ensure_capabilities
treats None as permission to proceed => a MALFORMED baked contract silently
disabled required-capability enforcement (the gate reports success exactly when
it can no longer verify anything). Replaced Option with a three-state
BakedManifest { Absent, Present(&'static Manifest), Malformed(&'static str) }:
Absent proceeds (no contract), Malformed HARD-FAILS with an actionable message.
- Macro output now uses fully-qualified paths (::edgezero_core::manifest::Manifest,
::std::sync::OnceLock) -- it expands in a downstream crate that may not have them
in scope.
FASTLY -- test seam was impossible as specced (verified against fastly-0.12.1):
- SendError has private fields + no public ctor; SendErrorCause is #[non_exhaustive].
NEITHER is constructible in a test, so "one test per SendErrorCause" was unwritable.
Also retracted my own earlier instruction to "match SendErrorCause exhaustively so a
future variant is a compile error" -- #[non_exhaustive] makes that impossible; a `_`
arm is mandatory. BackendCreationError is the opposite (not non_exhaustive =>
constructible AND exhaustively matchable). Documented the asymmetry and split
classification: a pure classify(SendFailure) over a locally-defined constructible
enum (unit-testable), plus a thin cause_to_failure boundary map covered by Tier 3.
- Purged the last mutex-era reasoning: "prior session" NameInUse cause (names are
session-scoped; a fresh session starts clean), "outer lock held continuously",
"no other thread", finer-grained locking. Now stated in single-threaded
session-map-borrow terms. [SUPERSEDED] marker added to the last appendix entry.
CLI:
- Manifest::configured_adapter_names() does not exist -> iterate
manifest.adapters.keys() (BTreeMap<String, ManifestAdapter>, manifest.rs:95;
ordered => deterministic failure reporting).
- The three config commands are NOT symmetric; replaced the over-general "same
applies to push/diff" with a per-command table: push = gate the TYPED path only
(bundled is an erroring stub, nothing to gate); validate = shared inner op both
entries call (the only one needing it); diff = never gated.
LINT SWEEP (spec pseudocode, same gate as Phase 1a):
- sent += chunk.len() -> saturating_add (arithmetic_side_effects).
- production cert_host().unwrap() -> explicit if-let (unwrap_used).
- dispatch_budget: added #[inline] + the lint contract; |dur| -> |duration|
(min_ident_chars), including the closure body.
PHASE 1a PLAN:
- The new variants must also join the retry_after_* and field_path_* matrices
(502/504 carry neither), not just kind_strings_per_variant -- three matrices.
…+ strip meta-refs from code comments
Verified each finding against the tree/compiler before fixing.
HIGH (self-inflicted lifetime bug): ensure_capabilities took BakedManifest, whose
Present holds &'static Manifest, but the four file-backed gate sites pass LOCAL
&Manifest borrows (not 'static) -> cannot compile. Introduced a lifetime-bearing
ManifestContract<'a> { None, Malformed(&'static str), Present(&'a Manifest) };
BakedManifest converts in via as_contract(); file-backed callers build it from
Option via from_opt. Fail-closed on Malformed preserved.
HIGH (StoredError): the "total" capture omitted NotImplemented + ServiceUnavailable
(EdgeError has 10 variants); MethodNotAllowed stored String not Method (forcing a
fallible reconstruction); the cancellation example used the REJECTED flat
{kind,message} shape; and three call sites used clone_as_edge_error() while the
method is to_edge_error(). All fixed.
HIGH (Fastly error classification, mutually-exclusive rules): creation step-6 prose
+ the §5.4 stage-1 row said "every non-NameInUse -> 502", contradicting the
per-variant table (clamp/name/encoding -> internal 500). And a "internal is never
correct during send" sentence contradicted SendFailure::LocalInvariant. The
per-variant tables are now authoritative everywhere; the blanket-502 prose is gone.
HIGH (Fastly cache unbounded): backend identity used EXACT ceiled budget_ms, which
drifts by the ms across requests (97/98/99 for a nominal 100ms deadline) while the
thread_local map is session-wide -> one leaked backend per distinct ms, unbounded.
Now identity uses budget_bucket_ms (ceil to a 1-2-5 grid); host timers armed with
the bucket (>= real budget, a looser backstop) while the exact deadline stays
enforced by the cooperative is_expired() check. Bounds distinct backends per host to
~a few dozen, independent of request count. Added a bound-assertion §5.4 row.
HIGH (Spin provisioning wrong-file): recommended re-reading from manifest_root, but
manifest_root = args.manifest.parent() DROPS the filename while --manifest accepts an
arbitrary name -> would read the wrong file for --manifest custom.toml. Now: thread
the hosts (or full path) into provision; a bare re-read does not work.
HIGH (outbound-host default): the initial manifest EXAMPLE still advertised the
cleartext-enabling ["*"] default the corrected schema rejects. Example + appendix now
state absent -> https-only ["https://*:*"], explicit ["*"] -> http+https opt-in.
MEDIUM: Manifest::configured_adapter_names() doesn't exist -> iterate
manifest.adapters.keys() (real BTreeMap field). The three config commands aren't
symmetric (per-command table). Response migration summary said buffering is reserved
for Body::Once on all three WASM adapters -- only Cloudflare streams lazily; Axum,
Fastly, Spin all buffer Body::Stream (fixed + appendix superseded marker).
MASTER-SPEC LINT: DEADLINE_FAR_FUTURE from_secs(7*24*60*60) -> from_hours(168) at both
live sites; Capability enum carries a documented #[expect(arbitrary_source_item_ordering)]
(semantic grouping tied to the matrix); added a prominent §3 directive that all code
blocks are illustrative and must be brought to the strict lint gate before use.
PHASE 1a PLAN: `let v` -> `body_json` (min_ident_chars, reviewer-verified); the three
error.rs tests are not all "exhaustive matrices" (only kind_strings_per_variant is;
the other two are subset checks) -- wording corrected. Deadline block replaced with
the version compile-verified GREEN under the repo's exact lint table.
COMMENTS: stripped section-number / round-N / phase meta-references from all code-block
comments (plan + 894 spec lines) -- comments describe the code, not the planning process.
Preserve Fastly progress behavior while ordering declarations for native CLI tests. Run Cloudflare guest contracts on pinned Node with scoped timer guards, a tested fixed-length stream fixture, exact framing assertions, and native request-body tripwires. Keep hosted qualification and the full outbound migration open.
Hard-cut prepared clients, scoped bodies and indexed batch receipts across all adapters and callers. Share preparation, finalization, revocation, task cleanup and deadline policy without compatibility paths. Preserve response limits, timeout provenance, partial results and direct native image optimization. Keep redirect following and hosted qualification open. Refs #436.
Integrate pre-dispatch hooks and request fidelity with route-aware admission and adapter-owned egress. Preserve hard-cut outbound APIs, audited decoder pins and the immutable signed Spin SDK source. Migrate fidelity fixtures, metadata wrappers, CI and documentation without restoring retired paths.
Add pending-head and pending-body cases to the existing Rust lifetime harness. Measure guest operation return before report formatting and validate host phases against actual response return before releasing origins. Preserve original cutoffs, strict timing failures and explicit host-head-only evidence without changing production outbound behavior or qualification claims.
Add bounded fixture CPU work, conservative clock and batch-epoch intervals, and source/receipt regression coverage. Preserve original cutoffs and report native timing misses as failed, unqualified characterization. Update the runbook and changelog without changing production APIs, runtime pins or capability claims.
Add one bounded fixture calibration send and a separate scheduling anchor without changing original deadlines or receipt targets. Prove A arrived during the send, preserve all-offset B checks, and reject impossible native payload ordering. Record mixed local runtime results as unqualified; production APIs, transport and pins remain unchanged.
Use one pinned harness-only Mio readiness owner per listener and explicit stop wakes. Preserve connection caps, parser budgets and joined cleanup, and reject admission after stop. Prove failed-wake fallback with watchdog/rescue regressions, retain four passing wire suites and disclose remaining strict timing misses without changing production contracts.
Include the underlying error in version-diagnostic assertions so an earlier subprocess failure is visible. Preserve the test conditions and command behavior; record the prior CI failure and successful local runner checks without assuming an unproven OS cause.
Align root, demo, fixture and generated SDK pins with the reviewed WASIp3 0.10 upgrade. Detach SDK task handles at one provider boundary while preserving existing cancellation ownership. Migrate native-request conversion coverage to the SDK executor. Record full local verification and retain strict lifetime timing failures as unqualified.
Summary
Implements the portable outbound HTTP and request-lifecycle contracts across core and the Axum, Cloudflare, Fastly, and Spin adapters.
start_batch_until, indexednext, explicitcancel, orderedcollect, and thesend_all_untilconvenience path.start_batch_untiltakes an observation cutoff: reaching it stops observation even when individual slots retain budget, while earlier cutoffs remain a supported partial-result operation.PlatformMetadatafacts for memory ceilings, live inbound request population, and host-ingress accounting, preserving source and explicit unknown reasons.Fits,Exceeds, and typedIndeterminateoutcomes; publishes canonical Axum, Cloudflare, Fastly, generic Spin, and Akamai Functions profiles.CompletedorCutoff. OnlyCutoffpermits unresolved slots in a successful result. Explicit adapter failure, premature driver EOF, and duplicate or out-of-range indices returnOutboundBatchFailure, preserving the precise error and every terminal slot collected before failure.MonotonicClock::deadline_afteranchors relative cutoffs to that clock. Equality expiry wins and backwards time fails closed.ResponseEgressCompletion, transferred directly throughPreparedIngress,ResponseEgressEnvelope, and the soleResponseEgressAttempt. Terminal state is stored before the response-scoped completion and global observer run, each exactly once and behind independent panic boundaries. No response-extension carrier or envelope extraction bypass remains.ResponseEgressCompletion::joincomposition so independent response-owned resources share one exactly-once terminal report, and hard-cuts detached normalized errors toSend { completion, deadline } | Abortwith request-start-relative bounded deadlines.AdmissionDecision::Abort. Axum closes the owned HTTP/1 connection before body polling; Cloudflare, Fastly, and Spin return a fixed provider error at their strongest available pre-response boundary and remain conservatively classifiedBestEffort.Hooks::configureplus core-ownedApp::build::<A>(platform). Successful hooks must preserve the complete adapter-selected metadata; core rejects changed facts instead of restoring them. Typed configuration failures propagate unchanged and stop serving before request handling. The generated scaffold keeps finite ingress policy with core-owned empty lifecycle tokens; the checked-in demo retains the full application-owned resource-composition example.AbortController, Fastly's low-levelstream_to_client, and Spin's WASI response/body/result writers.Current Checkpoint: Response Cleanup Failure Isolation
Checkpoint:
fbdc060f. Normal-completion/core terminal-injection coverage from15615dabis retained. See the D6 response-cleanup failure checkpoint for exact bounds and evidence limits.Earlier Checkpoint: Response-Owned Outbound Handoff
Checkpoint:
abbc2c11. Detailed local artifacts and failures are recorded in the D6 response-handoff tracker checkpoint.Earlier Checkpoint: Core Mixed-Observation Acceptance Binding
EZ-6/acceptance/22@unit/core, replacing its placeholder with five executable receipt/accumulator proofs. Run six exact three-slot cases: fresh and mixed check/next/collect under Completed, Cutoff and an original typed driver failure. Stage terminal production after both mixed receipts transfer.--lib outbound::; discover all 56 exact sentinels, including the original 51 redirect proofs. No new job or per-test CI list. The old redirect-only selection fails before execution because it omits the five batch proofs. Use the shared raw Cargo log path without relabelling older evidence.Checkpoint:
279c5a93.Earlier Checkpoint: Buffered Cap During Batch Progress
caps-while-waitinglifetime case through the production clients: 128 KiB decoded gzip trickling over 300 ms, independent 1 MiB decoded / 64 KiB buffered / 256 KiB encoded limits, healthy B/C siblings, and the original 34/534 ms batch timeline.Checkpoint:
8c11307d. Final local artifacts and individual failed timing preconditions are recorded in the D6 buffered-cap tracker checkpoint.Earlier Checkpoint: Reviewed Spin SDK Integration
6c59ed5b4fcb377072763aee0b27a3abdd8edb72(WASIp3 0.10.0 / wit-bindgen 0.63.0). Align the root, demo, reusable/request-fidelity fixtures, generator and all four application lockfiles.Checkpoint:
87b5e1ce. These are local regression results, not a frozen candidate or hosted qualification. D6 closure, native body-entry evidence, consumer acceptance and hosted qualification remain open. The acceptance inventory remains 32 ready / 479 planned and unqualified.Earlier Checkpoint: Fourth-Slot Per-Call Expiry
Implemented in cd8e77e4, within the approved D6 acceptance work:
Earlier Checkpoint: Readiness-Driven Test Origins
Implemented in d3f48564, under the requester's scoped fixture-accept approval:
CI diagnostic follow-up 11b8f2c4 adds underlying errors to two existing request-fidelity version-test assertions. The prior Linux workspace failure occurred before the collector tests; its OS/process cause was not exposed and remains unproven. The fresh run passes both the request-fidelity step and the complete shared-harness tests on Linux. No test conditions, retries, version policy or subprocess behavior changed; other CI checks and provider qualification remain separate.
Earlier Checkpoint: Same-Client CPU Calibration
Implemented in 0d09236a, under the requester's scoped calibration approval:
Earlier Checkpoint: CPU Stretch Characterization
Implemented in 44baaf3b:
Earlier Checkpoint: Pending Cutoff Lifetime Probes
Implemented in 73ed39ba, following the main integration:
host-head-written-only, not proof that the SDK consumed headers or entered a body read. Full criterion 16, remaining D6 work and hosted qualification stay open. Production outbound code, signed SDK/decoder pins, frozen requirements/interfaces and capability levels are unchanged; inventory remains 32 ready/479 planned and unqualified.Earlier Checkpoint: Coordinated Spin SDK Executor Upgrade
Implemented in 7b68f737:
e65ff90dadf4173b08b14194e878ad3acd555337; its DCO check passes. Upstream Rust 1.94 CI remains separate from local Rust 1.98.1 verification.spawn_local. Root, demo, reusable fixture and generator use identical Git URL/revision and package version; guards compare all three. Lock changes stay within the affected SDK/bindings/macro graph. Decoder/map contracts remain unchanged.qualified: false; full acceptance binding, candidate freeze and authorized hosted qualification remain open.Earlier Checkpoint: Begin/Batch Lifetime Probes and SDK Gap
Implemented in 834a009e:
wit-bindgen 0.57.1.qualified: false. CPU-stretch, remaining lifetime/registration/consumer acceptance, candidate freeze and authorized hosted qualification remain open.This is a verified test/diagnostic checkpoint, not a passing full lifetime suite or completed feature qualification.
Earlier Checkpoint: Standalone Test Harness and Redirect Bindings
Implemented in b5f94c70:
crates/edgezero-test-harness, with a non-published standalone workspace and its own lockfile. Remove the old runner package/directory without an alias or duplicate implementation. Keep deployable fixture apps separate and unit/contract tests in their owning crates.qualified: false. Readiness records executable assertions, not consumer adoption, applicability approval or exact-candidate qualification.This completes the approved runner relocation and local redirect binding checkpoint, not the whole adoption plan. Lifetime/registration acceptance, successful native optimizer transformation/redirect/performance evidence, consumer integration, candidate freeze and authorized hosted qualification remain open. Optional idle-body policy is not silently adopted. No deployment, capability promotion or production outbound behavior change occurs.
Earlier Checkpoint: Local Workerd Redirect Acceptance
Implemented in 4e64db99:
outbound-wirematrix and bounded two-origin servers as Spin. A fixture-only route obtains the production client from the canonical request context; no Fetch replacement, parallel outbound transport or new host-language runner is used.Successful native optimizer redirects, conditional idle-policy coverage, complete criterion-level binding and consumer/hosted acceptance remain open. The inventory remains eleven ready legs, 500 planned and
qualified: false.Earlier Checkpoint: Rust Spin Wire Harness
Implemented in 361da4f1:
--adapter spin --suite outbound-wirein the existing Rust fixture harness. No Python or Node replacement runner, compatibility wrapper, dependency changes or second process supervisor remains.This checkpoint is test-tooling only. No hosted deployment, capability promotion, candidate freeze or optimizer performance claim is made. The inventory remains eleven ready legs, 500 planned and
qualified: false.Earlier Checkpoint: Provider Redirect Boundaries
Implemented in 699cd49c:
Workerd/origin parity, successful optimized-first-hop redirects, conditional idle-policy coverage, complete criterion-level acceptance binding, candidate freeze and consumer/hosted qualification remain open. Native optimizer dispatch is preserved; unavailable-boundary controls do not establish transformation or performance. No deployment is performed. The inventory remains eleven ready legs, 500 planned and
qualified: false.Earlier Checkpoint: Fastly Cooperative-Wake CI Repair
Implemented in f57b8130:
qualification: false.Still open: provider-runtime redirect acceptance, exact-candidate criterion bindings, resource-policy decisions and consumer/hosted qualification. All previous outbound guarantees and direct native Image Optimizer integration remain intact.
Earlier Checkpoint: Explicit Redirect Following
Implemented in e02c2360:
HttpClient::followAPI with a required synchronous application policy and validated 1..=20 redirect limit. Plain Send, Begin and Batch remain unfollowed.Still open: other provider-runtime Follow parity, actual Fastly optimizer-hop mapping, consumer composition, exact-candidate criterion bindings, resource-policy decisions and hosted qualification. The inventory remains eleven ready legs, 500 planned and
qualified: false. No deployment or capability promotion occurred, and the PR remains draft. Current tracker.Earlier Checkpoint: Shared Outbound Pipeline and Ownership
Implemented in 2be1dd87:
Open at that checkpoint: explicit same-client redirect following (now implemented in the checkpoint above), remaining exact-candidate acceptance bindings, resource-policy decisions and hosted qualification. Local runtime evidence does not promote capabilities or prove end-client delivery. The PR remains draft. See the current tracker.
Earlier Checkpoint: Live Paired Time
Implemented in d86d4c0d, with the isolated Cloudflare test-ordering correction in f4d660b1:
with_time/ Axumtry_with_time, removing clock-only constructors without aliases.OutboundTimefor live asynchronous deadline handling. Check timers before source polling, preserve operational errors and diagnostics, retain clock high-water observations across chunks/yields, and release terminal/cancelled ownership.This completes only the approved sequencing checkpoint. The public head-first trait, settlement/context issuance, batch receipts, eager Begin, redirect integration and hosted qualification remain open. Requirements and frozen Plan 02 hashes are unchanged. See the verification audit.
Earlier Checkpoint: Bounded Synthesized Headers
Added in
d47c8026:This checkpoint is not completion of the broader adoption plan or hosted qualification. The full head-first/eager/redirect migration, Fastly's failed G2 preserving-read proof, all nine failed Fastly framing SDK parity controls, Cloudflare overflow recovery, consumer integration and exact-candidate hosted qualification remain open. No capability is promoted, no compatibility layer is restored, and the PR remains draft.
Reviewer correctness follow-up
Addressed R1–R4 in
92111839:Main merge and shared-policy follow-up
Addressed R5 and R6 in
91f4af38, merging main683202c6without restoring superseded APIs:OutboundBatch::from_futures; adapters retain preflight validation, exchange ownership, clocks, and terminal samples. Ready results remain retained, driver EOF/index invariants remain enforced, and cutoff releases pending futures before reporting termination. Fastly retains its distinct provider-handle driver.ResponseBodyPolicyandapply_response_body_policy, preserving declared-length checks, encoded limits, decoding/header rewriting, decoded limits, passthrough, and rechunk order. Transport reads, bodyless handling, deadlines, cancellation, and final buffering remain adapter-owned.Result<(), E>through a wrapper that prevents SDK-generated second responses.R7's dependency-audit-script deduplication remains deferred; dependency contract checks remain unchanged and pass.
Bounded native ingress and config follow-up
ConfigValuereturns. Axum constructors requireConfigStoreLimits;from_mapis fallible. Reads share payloads, and local config push updates require a server restart.serde_json 1.0.150, hash canonical JSON incrementally, and release extraction's raw handle before secret retrieval. A moving-reallocation malformed-root probe measured 36,428 requested peak bytes under a 100,060-byte staging cap, with zero retained bytes after rejection.Unsupportedand response egress remainsBestEffort. Local emulators and guest contract tests are not deployed runtime evidence.Bounded provider reads and lifecycle qualification infrastructure
FinalizationRegistrycleanup support. Generated and demo Workers explicitly enableenable_weak_ref, and regression tests inspect their TOML rather than assuming an exact one-flag string.try_opennow returns typedConfigStoreError.Downstream acceptance impact
Cloudflare
outbound-deadlinesandstreamed-upload-deadlinesareBestEffort, notNative. Downstream acceptance suites must use best-effort deadline wording and must not claim exact cancellation enforcement until deployed host-observed evidence exists.The response completion, ingress abort, and fallible application-assembly changes are intentional breaking changes. Consumers must provide a completion on every response-producing admission decision and propagate
App::build::<A>(platform)failures.Evidence boundaries
Capability declarations remain conservative where provider behavior is not proved:
Unsupportedon all adapters.Nativeon Axum andBestEfforton Cloudflare, Fastly, and Spin because finite host-side teardown is not proved or synchronously preemptible.Nativeon Axum's owned HTTP/1 connection andBestEfforton Cloudflare, Fastly, and Spin because provider transport reset/close is not directly observable.BestEfforton every adapter because no boundary proves end-client receipt; provider-specific blocking and teardown limits are documented.Unsupported; provider parsing/materialization, SDK copies, informational responses, trailers, allocator overhead, and other host-owned memory remain outside guest-visible caps.BestEfforton every adapter because provider operations are not proved cancellable within a finite wall-clock interval.BestEffortpending a deployed host-observed cancellation artifact. Fastly and Spin retain their separately documented deadline, phase-budget, and isolation limitations.Validation
This checklist records validation across PR checkpoints. The exact latest reruns and remaining acceptance failures are listed above; it is not a claim of hosted qualification or a fresh run of every historical matrix.
scripts/run_tests.shcargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace --all-targetscargo check --workspace --all-targets --features "fastly cloudflare spin"Closes
Closes #436.