Skip to content

feat: implement portable outbound HTTP and request lifecycle contracts - #275

Draft
aram356 wants to merge 477 commits into
mainfrom
docs/outbound-http-spec
Draft

aram356 wants to merge 477 commits into
mainfrom
docs/outbound-http-spec

Conversation

@aram356

@aram356 aram356 commented Jun 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Implements the portable outbound HTTP and request-lifecycle contracts across core and the Axum, Cloudflare, Fastly, and Spin adapters.

  • Adds buffered and streamed outbound requests/responses plus a hard-cut, completion-driven batch API: start_batch_until, indexed next, explicit cancel, ordered collect, and the send_all_until convenience path.
  • Clarifies that start_batch_until takes an observation cutoff: reaching it stops observation even when individual slots retain budget, while earlier cutoffs remain a supported partial-result operation.
  • Replaces the memory-only adapter seam with typed PlatformMetadata facts for memory ceilings, live inbound request population, and host-ingress accounting, preserving source and explicit unknown reasons.
  • Adds checked, scope-aware memory-envelope validation with Fits, Exceeds, and typed Indeterminate outcomes; publishes canonical Axum, Cloudflare, Fastly, generic Spin, and Akamai Functions profiles.
  • Hard-cuts retired platform-memory APIs, updates the demo and generator, and records exact signature migrations in the unreleased changelog.
  • Makes batch termination explicit as Completed or Cutoff. Only Cutoff permits unresolved slots in a successful result. Explicit adapter failure, premature driver EOF, and duplicate or out-of-range indices return OutboundBatchFailure, preserving the precise error and every terminal slot collected before failure.
  • Preserves per-slot elapsed time and failure provenance. A batch cutoff keeps completed slots and leaves unresolved slots explicit instead of converting them into request timeouts.
  • Adds explicit cache-bypass and wire-authority policies. URI routing, SNI, and certificate identity remain independent where the provider supports that separation.
  • Enforces independent encoded-transport, decoded-output, and final-buffer limits; header count/byte limits; Brotli window and decoder-state policy; opt-in response rechunking; and typed timeout, transport, protocol, and content-coding failures.
  • Uses fixed category-only public error messages so provider diagnostics, URLs, and credentials cannot become wire-visible.
  • Carries one application-owned monotonic clock through ingress, outbound dispatch and deferred streams, returned response drains, configuration extraction, and response egress. MonotonicClock::deadline_after anchors relative cutoffs to that clock. Equality expiry wins and backwards time fails closed.
  • Adds route-aware ingress admission on every standard adapter, stable route metadata, request-start-relative deadlines, opaque request-owned grants, and bounded 404/405 fallback reads with application-selected overflow/timeout responses.
  • Gives every response-producing admission decision one non-clone ResponseEgressCompletion, transferred directly through PreparedIngress, ResponseEgressEnvelope, and the sole ResponseEgressAttempt. 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.
  • Adds deterministic ResponseEgressCompletion::join composition so independent response-owned resources share one exactly-once terminal report, and hard-cuts detached normalized errors to Send { completion, deadline } | Abort with request-start-relative bounded deadlines.
  • Adds explicit non-response 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 classified BestEffort.
  • Hard-cuts application assembly to fallible Hooks::configure plus core-owned App::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.
  • Implements adapter-owned response delivery through each platform's strongest boundary: Hyper connection ownership on Axum, a Workers writer plus AbortController, Fastly's low-level stream_to_client, and Spin's WASI response/body/result writers.
  • Adds a closed Fastly lifecycle runner that keeps request conversion, routed execution, response finalization, framing, streaming, and platform delivery under adapter ownership.
  • Fastly dispatches eligible slots before harvesting and advances heads/bodies through one cooperative readiness multiplexer, preserving positional ownership, original deadlines, bounded raw reads, fair rotation and documented provider limitations.
  • Preserves duplicate response fields, canonical host authority, bodyless response framing, strict content-length validation, hop-by-hop stripping, and source release on every terminal path.
  • Hard-cuts superseded APIs and configuration error variants. The checked-in demo, generated applications, templates, guides, capability matrices, CI sentinels, and legacy guards use only the current contracts.

Current Checkpoint: Response Cleanup Failure Isolation

  • Extend the existing Fastly cleanup helper with intentional source failure and a real one-second egress-relative write deadline while the original three-second batch remains pending. Preserve normal completion, retained body-owned Begin/Batch scope errors, one terminal report, the pending callback sibling's K payload and fresh same-client P work.
  • Validate the exact response head/prefix before acknowledging a separate ACK gate. Only matching typed terminal evidence and interrupted wire delivery release the original sources. Require the intentional error or actual pending-poll witness; reader timeouts, malformed framing, capture failures and clean HTTP success cannot impersonate failed delivery.
  • Keep three bounded ordered records per case under one total 1 MiB log budget. Reject observed shrinkage and late/foreign/partial protocol records; validate the final tail after runtime shutdown. Normal/failure source evidence uses 26/30 events within unchanged limits. Fixture socket closure is not SDK-drop or host-cancellation proof.
  • Verification: all five root gates pass (2,568 tests / 32 binaries, one ignored), 275 harness tests (one ignored), 55 fixture-core tests and one native guest mode-parser test. Strict harness/core/all-four-target entrypoint Clippy, formatting, 51 Node regressions, docs/guards and dependency audits pass. Independent guest/source, validator and evidence reviews are complete; review regressions were reproduced before repair.
  • Final serial local runs retain all strict failures: Axum 7/10, repaired Viceroy 11/12, Spin 8/10 and workerd 7/10; every full suite remains exit 1. All three Fastly cleanup cases and the post-shutdown tail check pass. Async handoff still passes on the other three targets. Exact observations and earlier failed runs remain in the D6 tracker.
  • No production API, scheduler, transport, SDK/runtime pin, capability or frozen requirement change; no deployment. G1 stays 33 ready / 478 planned, unqualified. Full21/23, other adapter failure paths, SDK/body witnesses, strict timing, candidate freeze and authorized hosted/consumer qualification remain open.

Checkpoint: fbdc060f. Normal-completion/core terminal-injection coverage from 15615dab is retained. See the D6 response-cleanup failure checkpoint for exact bounds and evidence limits.

Earlier Checkpoint: Response-Owned Outbound Handoff

  • Add EZ-6 criterion 20 to the existing local lifetime runner for Axum, workerd and Spin. One exact response-body owner retains the original pending begun origin and three-slot batch across dispatch; the body collects those same handles, without replay or another transport.
  • Hold all source heads, payload and EOF until the native client validates a canonical, bounded NDJSON prelude. Require closed callback/live body scopes and Cloudflare/Spin's fixture-only dispatch-return signal. Validate causal source write-start order, strict original arrival cutoffs and valid late collection. This names EdgeZero dispatch/native handoff, not literal exported callback return or hosted delivery.
  • Harden the same HTTP reader against ambiguous framing, short/fragmented prefixes and notification after waiting for a gated tail. Keep exact two-record bounds, malformed-evidence controls, preexisting resource-owner counts and exactly-once terminal family release. Retain primary client failures, interrupted captures, cleanup errors and client/runtime logs.
  • Verification: all five root Rust gates pass (2,568 tests / 32 binaries, one ignored); 263 harness tests (one ignored), 54 fixture tests, strict harness/core/all-four-entrypoint Clippy, 51 Node guard tests, docs checks and dependency audits pass. Independent ownership, evidence, code-quality and verification reviews are clear.
  • Reviewed local handoff passes on Axum, Spin and workerd. Fastly records this criterion as inapplicable and still runs its original nine cases. Complete suites remain red: Axum 5/10, repaired Viceroy 5/9, Spin 6/10, workerd 5/10. Strict source/arrival/cadence misses and earlier failed observations are retained, not waived by the new case.
  • Inventory remains 33 ready / 478 planned, unqualified. D6, native body-entry witnesses, cleanup/callback cases, candidate freeze and authorized hosted qualification remain open. No production API, SDK/runtime pin, capability or frozen requirement changes; no deployment.

Checkpoint: abbc2c11. Detailed local artifacts and failures are recorded in the D6 response-handoff tracker checkpoint.

Earlier Checkpoint: Core Mixed-Observation Acceptance Binding

  • Activate only 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.
  • Preserve the earlier four-slot partial-failure/cutoff proofs and out-of-order test. Assert precise states before merge, cardinality and termination afterward, original error identity, no premature body consumption, exact payloads and one body-owner release. Invalid insert/merge recovery retains both inputs.
  • Broaden one existing grouped CI execution to --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.
  • Final verification passes all five root Rust gates: 2,568 tests in 32 binaries (one existing ignored), strict workspace Clippy, formatting and feature/Spin checks. The grouped outbound command passes 296 tests; 37 evidence-checker and 14 nonzero-guard tests pass, as do docs/tooling and dependency audits. The sandboxed full run denied existing TCP binds; unchanged loopback-enabled and final reviewed runs pass. Independent specification, tooling and code-quality reviews are clear.
  • Inventory is now 33 ready / 478 planned across the unchanged 255 criteria / 511 legs, still unqualified. All four adapter legs of criterion 22 remain planned. Prior runtime timing misses, native body-entry witnesses, response-owned/callback lifecycle cases, candidate freeze and authorized hosted qualification remain open. No production API, implementation behavior, SDK/runtime pin, capability, frozen requirement or deployment changes.

Checkpoint: 279c5a93.

Earlier Checkpoint: Buffered Cap During Batch Progress

  • Add the ninth caps-while-waiting lifetime 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.
  • Record original slot-error provenance before bounded report-body reads. Prove the written over-cap prefix and both healthy payloads precede guest-response return in the same host clock, and that return precedes the source ending. Retain exact partial-write evidence, bounded encoding/retries and strict source cadence; no guest dependency or extra production path.
  • Add negative controls for report-generated errors, impossible elapsed times, bunched writes, incorrect schedule labels and post-return sibling writes. Independent source, guest, validator and documentation reviews cover the checkpoint.
  • Verification: all five root Rust gates pass, including 2,568 tests in 32 binaries (one ignored); 252 shared-harness tests (one ignored), 51 qualification guest tests, strict harness/all-four fixture Clippy, formatting, docs lint/format, dependency/legacy/docs/inventory guards and 36 evidence-checker regressions pass.
  • Final local cap case passes on Axum, repaired Viceroy 0.21.0 and Spin 4.1.0. Workerd returns the correct original typed cap error early but fails its source-write precondition at 5.055 ms late against the unchanged 5 ms allowance. Preserve that failed run instead of substituting an earlier passing sample. Complete nine-case suites remain nonzero (8/9, 7/9, 7/9 and 6/9 respectively), with the other timing misses recorded in the tracker.
  • No production API, outbound transport, scheduler, SDK/runtime pin or capability changes. D6, native SDK body-entry witnesses, acceptance bindings, candidate freeze and hosted qualification remain open; inventory is still 32 ready / 479 planned and unqualified. No deployment or qualification promotion.

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

  • Consume the reviewed Spin SDK contribution at immutable fork revision 6c59ed5b4fcb377072763aee0b27a3abdd8edb72 (WASIp3 0.10.0 / wit-bindgen 0.63.0). Align the root, demo, reusable/request-fidelity fixtures, generator and all four application lockfiles.
  • Explicitly detach the SDK's new join handle at one private provider launcher. Preserve the existing scheduler, registration-before-spawn and cancellation ownership; do not add a compatibility entrypoint or another executor.
  • Migrate native SDK request construction in its WASM refusal contract to the SDK executor. Preserve the exact status/header/body and handler/middleware assertions. Update the dependency audit, design, implementation tracker and changelog together.
  • Local verification passes all five root gates (2,568 tests, one existing ignored), 35 Spin WASM contracts, 21 SDK-resource cases, the actual eager component, both strict Spin WASM Clippy configurations and all eight feature combinations. Demo tests/builds and the full ignored fresh generated-workspace gate pass, including all three WASM adapters and fallback TCP checks.
  • Spin 4.1.0 passes all 68 wire exchanges with 84 correlated origin receipts. The eight-case D6 lifetime run passes six cases but retains two strict timing failures: slow-origin Begin timing and the CPU B-write window. The complete run still exits 1; no target is relaxed or capability promoted.

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:

  • Add an eighth lifetime case through the same production clients. Preserve the original slow-origin case, original 534 ms cutoff and A/B/C schedules; only fourth slot D has a 300 ms per-call timeout. Late observation returns indices 0/1/3 with typed D timeout and exact lossless collection states. D's source never responds and closes boundedly; this is not SDK-cancellation evidence.
  • Validate C's actual late response or supported late peer close in both slow cases. Reject missing terminal evidence, early payload, extra writes, wrong timeout provenance and collection-stamped latency. Keep timeout-contract diagnostics separate from overall strict source-timing acceptance.
  • Verification: all five root Rust gates pass, including 2,568 workspace tests in 32 binaries (one ignored); 243 shared-harness tests (one ignored), 49 qualification guest tests, strict harness/guest Clippy and formatting, docs lint/format, legacy/docs/inventory guards and 36 evidence-checker regressions pass. Independent specification, cleanup, code-quality and evidence reviews are clear.
  • Reviewed local Axum, repaired Viceroy 0.21.0, Spin 4.1.0 and workerd 2026-09-10 runs all retain D's exact 300 ms PerCallTimeout and completed siblings. Only Spin passes the complete new case; all four eight-case suites exit 1 under unchanged strict timing targets. Source-timing misses remain visible, and no passing earlier sample replaces a failed final run.
  • No production API, transport, scheduler, runtime/SDK pin or capability change. Inventory stays 32 ready / 479 planned, qualified: false. Native body-entry evidence, remaining lifetime/redirect acceptance, candidate freeze and hosted/resource qualification remain open. No deployment or qualification promotion is authorized by this checkpoint.

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.

  • Replace the test origin's 5 ms accept sleep with one private, pinned Mio 1.2.1 readiness owner per listener. Register both listeners/wakers before spawning; drain accepts through WouldBlock and check sticky stop before accounting or worker creation. No alternate collector, raw-handle code, synthetic TCP wakes or production dependency changes.
  • Stop releases existing gates and attempts both wakes outside the accounting mutex. Each collector retains its wake owner until exit; a finite 50 ms poll fallback protects against missed/failed stop wakes. Preserve 128 cumulative/eight-active connections, existing parser/I/O/gate budgets and collector-before-worker joins. This is not a realtime or whole-shutdown guarantee.
  • Add colocated queue, wake-race, idle finish/drop, stop-after-accept, capacity, partial-header and failed-wake regressions. Synchronize both collectors past their last stop check before failure injection. Removing the poll timeout makes both watchdog tests fail cleanly around two seconds, with real-waker rescue and joined cleanup; restore the finite timeout before all passing runs.
  • Verification: all five primary Rust gates, 2,568 workspace tests (one ignored), 238 harness tests (one ignored), 48 qualification-enabled fixture tests, strict harness Clippy/formatting, docs lint/format, legacy/docs/inventory guards and 36 evidence-checker regressions pass. Independent code, test and documentation reviews are clear; forced failure of the second OS thread spawn remains reviewed rather than directly injected.
  • All four ordinary outbound-wire suites pass: 68 exchanges / 84 correlated origin requests each. Final seven-case timing runs remain failed/unqualified: four controls pass per target; fast-origin passes on Axum and repaired Viceroy; only Spin's CPU case passes. Slow-origin misses remain on every target. Retain actual all-offset failures rather than selecting earlier passing runs or relaxing targets.
  • Fixture accept polling is now implemented. Remaining timing/native SDK body-entry evidence, measurement-approval and hosted qualification gates stay open. Requirements, public APIs, production transport/code, SDK/runtime pins and capabilities are unchanged. Inventory remains 32 ready / 479 planned, unqualified; no deployment or candidate freeze is authorized.

Earlier Checkpoint: Same-Client CPU Calibration

Implemented in 0d09236a, under the requester's scoped calibration approval:

  • Add one buffered calibration Send through the same injected client after origin consumption: 64-byte cap, one-second timeout, absolute request-start-relative 520 ms safety deadline, bodyless 204. A/B/C wait boundedly for its causal sample; only a scheduling estimate uses the midpoint.
  • Keep A's entire arrival interval inside that EdgeZero Send. End the calibration hold at 450 ms plus the causal upper offset bound, then use the original timer for next500. Preserve A180/B530/C900, batch34/cutoff534, CPU520..560, all-offset source proof, invocation-epoch bracketing and strict SDK receipt cutoffs.
  • Close an independently reproduced evidence defect: calibration receipt cannot precede the source payload's pre-write sample. This proves necessary host ordering, not peer receipt; a post-flush sample could incorrectly reject legitimate preemption.
  • Verification: all five primary Rust gates, 2,568 workspace tests (one ignored), 229 harness tests (one ignored), 48 qualification-enabled guest tests, strict native/three-target fixture Clippy, formatting/docs lint, legacy/docs/inventory guards and 36 evidence-checker tests pass. Independent guest, host and documentation reviews are clear.
  • Final local CPU characterization: workerd passes unqualified; Axum, repaired Viceroy and Spin fail the full B-write timing proof. All four complete seven-case suites still exit 1 because earlier slow/fast timing misses remain; four controls pass per target. Retain the earlier intermediate runs rather than selecting passing samples.
  • The shared origin's existing 5 ms accept polling adds fixture-side uncertainty. Its bounded readiness/wake replacement is separately proposed, not implemented or approved here. No production API/transport/pin/capability changes, deployment, candidate freeze or hosted qualification. Inventory stays 32 ready / 479 planned, unqualified; native SDK body-entry evidence and remaining acceptance gates stay open.

Earlier Checkpoint: CPU Stretch Characterization

Implemented in 44baaf3b:

  • Extend the existing Rust harness/guest to seven lifetime cases with bounded synchronous CPU work from 520 to 560 ms, the original 34/534 ms batch start/cutoff, B's unchanged 530 +/-5 ms source target, and both permitted B outcomes. No new production API, transport, runtime pin or capability change.
  • Require the complete actual B write to fit every possible causal host/guest clock offset. Bracket core's batch epoch by the full measured invocation before reconstructing receipt arrival; equality at the original cutoff cannot pass. Reject collection-stamped arrivals, early C bodies hidden by late closure, frozen/backwards clocks and finite-work exhaustion.
  • Verification: all five primary Rust gates pass, including 2,568 workspace tests (one ignored); 223 shared-harness tests (one ignored), 47 qualification-enabled fixture tests, native/three-target fixture Clippy, formatting, docs lint, legacy/docs/inventory guards and 36 evidence-checker tests pass. Independent reviews found and closed the batch-epoch proof issue.
  • Local runtime characterization remains failed/unqualified on all four targets: each has four passing controls, the earlier slow/fast timing misses, and a CPU source-timing precondition failure. Cold-connection clock bounds are too wide/late to prove B's required write. These failures do not establish a production batch defect and do not qualify the CPU scenario.
  • Fixture-only clock calibration is proposed, not approved or implemented. Native SDK body-entry evidence, remaining lifetime/redirect acceptance and hosted qualification stay open. Inventory remains 32 ready / 479 planned, unqualified. No deployment or candidate freeze is authorized.

Earlier Checkpoint: Pending Cutoff Lifetime Probes

Implemented in 73ed39ba, following the main integration:

  • Extend the existing Rust harness and production-client guest to six lifetime cases, adding pending heads and host-written heads with withheld bodies. Preserve the original request-start-relative 200 ms observation cutoff, genuinely Pending polling, three unresolved slots and the 205 ms return target.
  • Fix two independently reproduced evidence bugs: record operation return before formatting the report, and require host phases before actual guest-response return rather than merely before a later fixture release. Preserve event-index ordering even when timestamps tie.
  • Verification: all five primary Rust gates, 212 shared-harness tests (one ignored), 43 qualification-enabled fixture tests, strict native/three-target fixture Clippy, formatting and legacy/docs/inventory guards pass. The existing CI groups cover these tests; no per-case workflow is added.
  • Local characterization: the two new cases pass on Axum, repaired Viceroy 0.21.0, Spin 4.1.0 and workerd, returning between 201.179 and 204 ms. Each complete six-case run still exits 1 because its original slow/fast timeline cases miss strict targets. Do not present these as passing six-case inventories or widen tolerances.
  • Pending-body evidence remains explicitly 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.
  • Main integration preserves route-aware admission and adapter-owned egress while adding request fidelity and pre-dispatch hooks; the signed SDK pin and existing outbound implementation remain intact. The current checkpoint does not rerun the expensive ignored generated-workspace matrix.

Earlier Checkpoint: Coordinated Spin SDK Executor Upgrade

Implemented in 7b68f737:

  • Create the public Stackpop Spin SDK fork and submit spinframework/spin-rust-sdk#169. Pin the signed SDK revision e65ff90dadf4173b08b14194e878ad3acd555337; its DCO check passes. Upstream Rust 1.94 CI remains separate from local Rust 1.98.1 verification.
  • Upgrade the coordinated chain to WASIp3 0.9.0 / wit-bindgen 0.62.0, retaining WASI 0.3.0 and one SDK executor. The upstream nested-spawn regression fails on the old chain and passes after this change. No observer-driven producers, second executor, vendoring or compatibility alias.
  • Hard-migrate callers to 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.
  • Verification: all five root Rust gates; both affected Spin WASM strict Clippy combinations; 35 Spin WASM contracts, 21 resource tests and the eager ownership component on exact Wasmtime 48.0.2; 60 demo-core, 42 fixture-core, 33 fresh-scaffold-core tests, demo/scaffold Spin compile checks and scaffold Spin Clippy; docs/build/dependency/legacy/evidence guards pass. The expensive ignored full generated-workspace gate was not rerun locally.
  • Actual Spin 4.1 wire controls pass 68 exchanges/84 receipts. Slow-origin batch starvation is removed. The four fixed lifetime cases remain two passes and two strict timing failures; do not widen tolerances or extend deadlines. A pre-existing host worker diagnostic was reproduced after requested shutdown with identical guest artifacts; retain that limitation, not a clean-hosted-shutdown claim.
  • Keep the frozen requirements/Interfaces, all outbound guarantees and capability values unchanged. Inventory stays 32 ready/479 planned and 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:

  • Add four fixed EZ-6 lifetime scenarios to the standalone Rust harness and existing production-client guests. Reuse the bounded origin parser/collector, process supervision and cleanup; no new transport, executor or host-language runner.
  • Preserve actual host receipts/writes, measured guest observations, original cutoffs, arrival elapsed, typed mediator errors and exact mixed collection states. Add conservative quiet-window clock bounds, cold-entry and timestamp-reanchoring regressions, and late-cleanup failure coverage.
  • Verification: 203 harness tests, 42 fixture-core tests, the five root Rust gates, strict harness/fixture lint, docs and legacy/evidence checks pass. The existing Axum redirect regression remains 68 exchanges/84 receipts.
  • Actual local lifetime results: Begin/unobserved-batch cases 4/5 pass on all four adapters. Slow/fast timeline cases 1/2 do not pass their strict timing targets. Spin additionally loses the slow-origin batch before dispatch; tracing confirms newly spawned work can wait until the origin responds in pinned wit-bindgen 0.57.1.
  • Remove all temporary diagnostics. A compatible audited SDK scheduler repair remains an explicit dependency decision, not an observer-polling workaround or another executor. No production API, SDK/runtime pin or capability claim changes in this checkpoint.
  • Keep all new lifetime bindings planned: inventory remains 32 ready/479 planned and 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:

  • Move the shared native Rust runner to 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.
  • Preserve fixture cwd, target directories, staging, selected runtimes and evidence paths. Record the fixture and runner locks as different artifacts. Preserve all 23 runner registry package versions/checksums and all 329 retained fixture-lock entries; the root lockfile and SDK/decoder/runtime pins are unchanged.
  • Fix launcher cwd selection so repository-pinned tools work when invoked from outside the checkout. Verify both an unrelated working directory and paths containing spaces with real Axum exchanges. Cargo metadata regressions prove isolation from both production and fixture dependency graphs.
  • Extend the same wire matrix to Axum and Fastly: all four adapters pass 68 exchanges/84 ordered origin requests. Preserve Spin's bodyless/upload/scope/SDK-terminal controls, Fastly smoke exit 0 and its separate 12/18 plus 23/43 progress inventories with unqualified exit 2.
  • Strengthen existing core redirect tests with the literal signed-date header, first-hop cancellation and two-hop query-redaction controls, preserving the older tests. Bind EZ-9 unit criteria 01-15 to all 51 guarded core tests and runtime criteria 16/17 to six exact environment legs.
  • The inventory is now 32 ready legs and 479 planned, with qualified: false. Readiness records executable assertions, not consumer adoption, applicability approval or exact-candidate qualification.
  • Update active scripts, guides, changelog, tracker and CI fetch/cache/fmt/lint/test commands for separate workspaces. Axum/Fastly/Spin execute the declared runtime paths with strict inventory guards; workerd runtime CI activation remains explicitly pending tool setup, separate from its passing local execution.
  • Verification: 185 standalone harness tests, 35 fixture-core tests, 36 evidence-checker tests, 51 guarded core redirect tests, all five root Rust gates, strict fixture/harness lint, docs checks, dependency/legacy guards and workflow lint pass. The one ignored Rust subprocess helper is explicitly exercised by parent tests. Independent reviews have no remaining concrete findings.

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:

  • Run Cloudflare through the same Rust outbound-wire matrix 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.
  • Pass all 68 local workerd exchanges and 84 ordered origin receipts. Assert exact bytes, coding and cap parity, manual redirects, unread intermediate-body progress, stopped/limited final bodies, cross-origin hops, streamed final responses and terminal metadata.
  • Preserve exact provider error classification: workerd requires 502 Transport at ResponseBodyRead with the observed sanitized Unknown cause; Spin retains its existing Protocol expectation. Negative controls reject wrong reasons, phases, causes and status.
  • Record the selected workerd executable/version and effective compatibility settings. Local execution uses Wrangler 4.131.0, workerd 2026-09-10 and worker 0.8.5 with the existing compatibility date/flags. Rebuild only the exact cached wasm-bindgen 0.2.129 CLI offline in a temporary directory; no global tools or project pins change.
  • Extend the common cleanup scope before local binding/build/startup work. Preserve primary failures, clean partial state and finish runtime/origins before publishing an inventory. Accept wrapper exit 143 only after confirmed sent TERM; premature 143 and failed/absent signaling remain rejected by regression controls.
  • Independently review the probe and host lifecycle changes. Pass 35 fixture-core tests, 184 harness tests, strict fixture/native/provider-target checks, all five root Rust gates, documentation checks and dependency/evidence/legacy guards. The intentional Rust subprocess helper remains parent-test-driven.
  • Rerun Spin's unchanged 68/84 inventory and bodyless/upload/scope/SDK-terminal controls. Preserve Fastly's 12/18 and 23/43 progress inventories with their unqualified exit 2. Missing workerd tools produce unsupported exit 2 and no success inventory.
  • Update the changelog, fixture guide and master tracker. No production API, SDK/runtime pin, capability promotion, hosted deployment or candidate freeze occurs.

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:

  • Replace the Python wire runner with --adapter spin --suite outbound-wire in the existing Rust fixture harness. No Python or Node replacement runner, compatibility wrapper, dependency changes or second process supervisor remains.
  • Reuse bounded raw HTTP parsing/capture, two tracked origins, file-backed runtime logs and process-group ownership. Preserve the unchanged Spin guest and all 68 exchanges/84 correlated origin requests, plus bodyless, upload, canonical-scope and attached Begin/Batch handoff controls.
  • Fail closed on late worker errors, malformed framing, non-text wire payloads, altered raw handoff bytes, premature runtime exits and cleanup failures. Keep unconfirmed cleanup retryable and reap timed-out signal helpers within bounded deadlines.
  • Verify 180 Rust harness tests and 29 qualification-enabled fixture-core tests. The one ignored Rust subprocess helper is explicitly invoked by its parent regression tests. Independent network and process-cleanup reviews report no unresolved findings.
  • Pass all five root Rust gates, strict fixture Clippy/format, documentation lint/format/build, workflow lint and dependency/evidence/legacy guards.
  • Rerun the actual pinned Spin runtime with exit 0 and the complete 68/84 inventory. Preserve Fastly smoke exit 0 and both progress inventories (12 probes/18 receipts plus 23 probes/43 receipts) with their required unqualified exit 2.
  • Keep the same CI job and pinned runtimes. Validate the Spin inventory predicate against complete evidence and missing-case, wrong-count and cleanup-failure controls. Preserve all production outbound contracts, frozen requirements and qualification status.

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:

  • Exercise the actual clients through the shared Send/Follow pipeline: Cloudflare guest fetch fixtures, local Spin HTTP wire controls and the explicitly selected repaired Viceroy.
  • Pass the complete Cloudflare suites: 82 guest library tests, 29 contracts and 12 Node fixture tests. New cases cover coding/cap parity, unread intermediate bodies, final stream behavior, original deadlines, drop cancellation and fixture cleanup. Guest abort observations are not workerd cancellation evidence.
  • Pass 68 Spin redirect exchanges with 84 origin-observed requests, preserving the existing bodyless, upload, canonical-scope and attached Begin/Batch handoff controls. Teardown joins request handlers before validating their outcomes and rejects unexpected runtime exits. Both false-green paths reproduce before the fix and are rejected afterward.
  • Preserve Fastly's original 12 progress probes and 18 origin receipts; require an additional 23 redirect probes and 43 receipts. Validate exact typed cap causes, normal-decoding unsupported-token passthrough, two-origin behavior, pending unread heads and native optimizer unavailable boundaries.
  • Close the reviewed evidence-validation gap: correlate overflow Follow metadata or exact hop-failure indices, reject contradictory/missing/foreign data, and preserve the original inventories. The new regression fails before the fix; all 149 harness and 29 qualification-enabled fixture-core tests pass afterward.
  • Run the Spin wire harness in its existing CI job using the checksum-verified pinned CLI. Extend Fastly's existing predicate without adding another job or weakening its required unqualified exit-2 disposition.
  • Pass all five root Rust gates, affected WASM clippy/build checks, docs lint/format/build, workflow lint and dependency/evidence guards. Core remains at 1,044 passing unit tests.
  • Update the changelog, fixture guide and requirement tracker. No production API, SDK/decoder/runtime pin or capability guarantee changes in this checkpoint.

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:

  • Reproduce the red reusable-HTTP smoke locally: the first real outbound origin request stalls after earlier lifecycle probes pass. The dependency future-incompatibility warning is not the failure.
  • Fix the existing Fastly progress loop to honor cooperative source wakes before host selection. A yielding body must not enter an empty-handle wait until its 30-second deadline.
  • Preserve direct/nested progress, retained older wakers and current-caller notification, expiry publication and normal waiting on later pending polls. Retain only a shared notification flag in progress state, not a parent task waker.
  • Add three colocated deterministic regressions. The direct/nested and retained-waker assertions fail before the fix; all thirty progress tests pass afterward.
  • Verify the exact unchanged Fastly smoke with exit 0 across every existing variant and assertion. All five root gates, four Fastly WASM clippy configurations, 265 guest library tests and 36 contracts pass.
  • Verify all twelve production-client progress probes and eighteen expected origin requests using the unchanged CI assertions. Its expected exit 2 preserves qualification: false.
  • Update changelog and tracker. No new CI job, public API, deadline, read-size policy, runtime pin, capability promotion or hosted deployment.

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:

  • Add the approved HttpClient::follow API with a required synchronous application policy and validated 1..=20 redirect limit. Plain Send, Begin and Batch remain unfollowed.
  • Route every hop through the same supplied client, prepared dispatch and shared head finalizer. Preserve method/replay precedence, original Location evidence, unchanged request bounds, typed hop errors and arbitrary application-policy errors.
  • Replay buffered Bytes by sharing storage; never buffer streamed uploads for replay. Drop followed heads unread, without draining, decoding or applying final-body caps to intermediate payloads.
  • Preserve the original absolute chain deadline through replacement requests and final streamed consumption. Retain per-hop timeouts/defaults, injected time, B31, indexed batches and direct native Image Optimizer dispatch without another fetch.
  • Strip only Authorization, Cookie and authority overrides from automatically prepared cross-origin hops; deliberate policy replacements remain application-owned. Demo and generator enforce initial and per-hop same-origin policy.
  • Close independently reproduced scope-reentrancy and backwards-clock gaps in shared preparation/source binding/finalization, including terminal cleanup. No compatibility transport, second collector or adapter-specific cancellation engine.
  • Verification: all five root gates; 1,044 core unit tests and 14 integration tests, including 50 redirect cases; 261 native TCP library tests plus 27 contracts; 60 demo-core tests; 33 fresh generated-core tests and strict generated-workspace clippy. The separate expensive ignored full-generator gate was not rerun locally.
  • Guest verification on final source: all eight WASM feature clippy configurations; Cloudflare 74 unit/29 contract cases plus nine Node fixture cases; Spin 35 contracts/21 SDK-resource cases and an executed eager-exchange component; Fastly 262 unit/36 contract cases with zero filtering under the approved repaired Viceroy. Fastly uses the system CA bundle with TLS verification intact, not a runtime-pin promotion.
  • Update guides, capability declarations, changelog, tracker and documentation guards. Frozen requirements, approved Interfaces and audited dependency/runtime pins are unchanged.

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:

  • Hard-cut prepared dispatch, paired time/progress, eager Begin and identity-bound batch receipts. Core shares validation, GET/HEAD settlement, normalization, decoding, limits, revocation, task cleanup and Body source binding; adapters retain provider I/O and scheduling.
  • Capture complete unpolled operations at invocation. Accept exact provisional Body owners before egress policy/framing, preserve response completion through fallback terminal, and keep source EOF distinct from terminal transmission.
  • Make nonblocking readiness observation SDK-pump-free. Preserve original deadlines, timeout causes, partial results, cutoff equality, independent caps, pooling and direct native Image Optimizer integration.
  • Retire Fastly's separately approved preparation-slack rejection while retaining B31 and actual expiry/backwards-clock checks. No compatibility aliases, duplicate lifetime registry or alternate transport remains.
  • Migrate all adapters, supplied clients, demo, generator, fixtures, guides, changelog and guards atomically.
  • Local verification: five primary Rust gates; 984 core unit tests; 261 native TCP cases plus 25 contracts; Cloudflare 74 guest unit/29 contract cases; repaired-Viceroy Fastly 262 unit/36 contract cases, zero filtered; Spin 56 guest cases. Refreshed demo default-core 55, rendered scaffold 28 and qualification fixture 29 tests pass. Strict provider lint and documentation/dependency/legacy guards pass.
  • Record the scoped interface authorization separately from historical exact-digest approvals. Normative requirements and audited SDK/decoder pins are unchanged.

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:

  • Hard-migrate all four clients and injectors to with_time / Axum try_with_time, removing clock-only constructors without aliases.
  • Use OutboundTime for 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.
  • Integrate timer-only waits into Fastly's existing progress multiplexer. Bound nested engines by original ancestor deadlines, isolate expired mailboxes, and wake outside state borrows.
  • Preserve B31, cutoffs/partial results, limits, coding/provenance and direct native Image Optimizer dispatch. Demo/templates remain aligned through standard injection.
  • Local verification passes: five required Rust gates, 2,293 workspace tests (one existing ignored test), strict provider WASM Clippy, Cloudflare 53 library/29 contract cases, Fastly 238 library/36 contract cases, and Spin 15 SDK-resource cases on Wasmtime 48.0.2. Demo, fixtures, documentation and dependency/legacy checks pass.
  • Actual-client Fastly receive probes and Spin local component functional cases pass, but both smoke suites correctly remain unqualified (exit 2). No hosted deployment or capability promotion occurred.

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:

  • Immutable app-level and validated Fastly per-request response-header sets, bounded to 32 values and 4096 serialized field-section bytes. Preserve canonical field precedence, ordered repeated values, typed overflow retry suppression, completion ownership and fallback budgets.
  • Cover normalized ingress errors, admitted source errors, canonical 404/405 and adapter fallbacks; do not decorate matched application responses/errors, admission refusals or failed application assembly.
  • Align the demo, generated application, specifications, implementation plans and changelog.
  • Local verification: all five required Rust gates, 2,193 workspace tests, 99 fixture tests, strict provider-target Clippy and documentation checks pass.
  • Actual local runtime evidence: 55 header wire cases, 40 assembly wire exchanges and one Axum pre-listener startup control pass. HEAD payload absence requires clean EOF; Fastly reports are request-correlated, and failed state release is observed before retry.
  • Evidence: header matrix, assembly and repeated header matrix.

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:

  • Axum validates exact-length source EOF before handing Hyper the final frame, validates transmitted zero-length streams before handoff under the original deadline, and preserves detached fallback payloads with HEAD suppression.
  • Batch completion requires clean driver EOF; trailing failures and malformed events are checked even after the last item or with zero slots. Collected slots survive driver failure.
  • Application assembly rejects changed platform metadata after successful configuration while preserving hook-error precedence.
  • Added regression coverage and aligned specs, implementation plans, guides, changelog, and the docs checker. Local workspace tests (720 core unit tests), strict native/WASM clippy, explicit adapter contracts, and demo/docs checks pass. Local WASM results are compilation/lint evidence; hosted lifecycle qualification requires separate deployed runtime artifacts.

Main merge and shared-policy follow-up

Addressed R5 and R6 in 91f4af38, merging main 683202c6 without restoring superseded APIs:

  • Axum, Cloudflare, and Spin share 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.
  • All four adapters share ResponseBodyPolicy and apply_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.
  • Main's opt-in reusable application lifecycles now use fallible assembly, fresh request registries, canonical admission, and lazy owned egress. Fastly custom callbacks return terminal Result<(), E> through a wrapper that prevents SDK-generated second responses.
  • Migrated the reusable fixtures to the current SDK pins and hard-cut APIs; expanded the legacy guard and CI coverage. Updated the outbound specification, lifecycle specification, implementation plans, guides, and changelog.
  • Local verification passes: full workspace tests (733 core unit tests), strict native/provider-WASM Clippy, Cloudflare/Fastly/Spin runtime contract suites, reusable fixture tests (5 core and 26 harness), Fastly fixture finalization and Viceroy 0.21.0 HTTP smoke, demo host/lint/WASM checks, fresh generated-project compilation, and docs/dependency/legacy gates.
  • Physical partial delivery is proved by a client-observed handshake before upstream truncation. Immediate zero-byte source failures are verified through exactly-once correlated completion with no fallback; only no-status EOF/reset is accepted, never timeouts or malformed responses. Local smoke evidence does not promote deployed capability guarantees.

R7's dependency-audit-script deduplication remains deferred; dependency contract checks remain unchanged and pass.

Bounded native ingress and config follow-up

  • Axum owns a bounded HTTP/1 accept loop and parser boundary: finite live connections, joint wire-head bytes, field count, parser buffers, absolute header-read deadlines, and idle keep-alive expiry. Capacity is acquired before parser construction; saturation closes the one accepted overshoot without spawning a refusal task. HTTP/2 is not enabled.
  • Axum loads immutable local config snapshots before listener binding. Validated file, entry, decoded key/value, registry-residency, and sequential startup-staging limits are enforced before owned snapshot allocation. Duplicate keys are rejected; allocation overflow fails startup; unavailable declarations remain typed failed bindings.
  • Hard-cut all four adapters and fixtures to shared UTF-8 ConfigValue returns. Axum constructors require ConfigStoreLimits; from_map is fallible. Reads share payloads, and local config push updates require a server restart.
  • Pin the parser allocation audit to 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.
  • Add real local TCP tests for HEAD/204/304 suppression, stalled producers, non-reading clients, disconnects, exactly-once completion/resource release, and connection-capacity recovery. These prove the adapter-owned native boundary, not confirmed client delivery.
  • Align the demo, generator, migration guide, specs, plans, and changelog. Fresh local verification passes: full workspace tests (737 core unit tests), strict native and three-provider WASM Clippy, demo tests/lint, generated all-adapter host/WASM builds, docs checks, and dependency audits.
  • Hosted config preallocation and response-lifecycle qualification remain open. Provider whole-value materialization and complete RSS accounting are not certified; broad config-allocation support remains Unsupported and response egress remains BestEffort. Local emulators and guest contract tests are not deployed runtime evidence.

Bounded provider reads and lifecycle qualification infrastructure

  • Cloudflare bounded config reads now use caller-sized BYOB byte streams, strict UTF-8, independent byte allowances, clock checks and cooperative task yields. Reader termination cancels the stream; an acquisition owner also cancels a stream arriving after the waiter was abandoned. Cancellation rejections cannot publish provider diagnostics.
  • Bounded KV dispatch requires FinalizationRegistry cleanup support. Generated and demo Workers explicitly enable enable_weak_ref, and regression tests inspect their TOML rather than assuming an exact one-flag string.
  • Fastly uses the pinned config-store host ABI with one initialized caller-sized buffer and no oversized-buffer retry. Root/pointer/chunk limits, declared-total preflight, transport integrity and reassembly bounds precede envelope materialization. Duplicate reserved discriminators fail closed, deadline expiry wins metadata errors, and try_open now returns typed ConfigStoreError.
  • Publish conservative requested-payload charges and retain explicit exclusions for opaque provider buffering, JS GC retention, synchronous interruption and complete allocation certification.
  • Add default-off qualification applications through all four standard adapter entrypoints, an allowlisted finite HTTPS probe runner and a fail-closed correlated ownership-evidence validator. CI runs the validator/fixture unit tests and compiles every qualification target without deploying anything.
  • Local verification: workspace tests, strict native and three-provider WASM Clippy, feature checks, Cloudflare WASM tests (28), Fastly bounded-config Viceroy tests (18), fixture tests (7 core/26 harness), validator tests (5), demo-core tests (48), dependency/legacy/documentation checks and docs build. A temporary native Axum fixture exercised nine normal/bodyless/failure/deadline scenarios with one terminal report and one resource release each, then was stopped.
  • Still open: a bounded Spin host/backend interface or independently verified read-allocation ceiling, plus actual hosted qualification with authorized isolated targets and complete revision-pinned telemetry. The validator qualifies only ownership for scheduled probes; it does not authenticate telemetry, prove same-instance recovery, confirm client delivery or promote capabilities.

Downstream acceptance impact

Cloudflare outbound-deadlines and streamed-upload-deadlines are BestEffort, not Native. 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:

  • Raw request-target/header accounting and parser-boundary CL/TE rejection are Unsupported on all adapters.
  • Inbound read deadlines are Native on Axum and BestEffort on Cloudflare, Fastly, and Spin because finite host-side teardown is not proved or synchronously preemptible.
  • Ingress admission abort is Native on Axum's owned HTTP/1 connection and BestEffort on Cloudflare, Fastly, and Spin because provider transport reset/close is not directly observable.
  • Response-egress abort, backpressure, completion, and write deadlines remain BestEffort on every adapter because no boundary proves end-client receipt; provider-specific blocking and teardown limits are documented.
  • Complete outbound and configuration-read allocation accounting is Unsupported; provider parsing/materialization, SDK copies, informational responses, trailers, allocator overhead, and other host-owned memory remain outside guest-visible caps.
  • Configuration-read deadlines are BestEffort on every adapter because provider operations are not proved cancellable within a finite wall-clock interval.
  • Cloudflare outbound deadlines and streamed-upload deadlines remain BestEffort pending a deployed host-observed cancellation artifact. Fastly and Spin retain their separately documented deadline, phase-budget, and isolation limitations.
  • Batch completion/cancellation/isolation and authority-override support are reported independently per adapter in the public capability matrix.

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.sh
  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace --all-targets
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • Native and WASM adapter feature matrices for Axum, Cloudflare, Fastly, and Spin
  • Fastly WASM/Viceroy contract and runtime suites
  • Reconciled reusable-app fixture tests, provider-target lint, and Fastly lifecycle HTTP smoke
  • Fresh generated all-adapter project host tests and all provider WASM builds
  • Checked-in demo host tests, strict Clippy, and all provider WASM builds
  • Documentation format, lint, build, contract, dependency, and legacy API guards

Closes

Closes #436.

aram356 added 2 commits June 7, 2026 00:01
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.
@aram356 aram356 changed the title Outbound HTTP design spec (midbid driver) Outbound HTTP design spec Jun 8, 2026
@aram356
aram356 marked this pull request as draft June 8, 2026 06:55
aram356 added 2 commits June 8, 2026 00:02
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.
@aram356

aram356 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

Round 50 — Fastly SDK correctness pass (commit 647489a)

Reviewer flagged four normative claims about Fastly that didn't match the actual SDK / public API. All fixes verified against docs.rs/fastly and Fastly's public Backend API docs before edit.

HIGH — lazy-streamed-response-passthrough on Fastly: downgraded Native → BestEffort. Response::with_streaming_body does not exist (only on Request). Response::stream_to_client() is the real API but is incompatible with #[fastly::main]. Default scaffold falls back to buffered passthrough; the non-#[fastly::main] entry-point template is tracked in new §8 risk 12.

HIGH — NameInUse semantics: rewritten. Fastly's session-uniqueness rule is unconditional — no "identical re-registration returns Ok" carve-out. Backend::from_str(name) returns a handle only, no property inspection. NameInUse on a name not in this adapter's collision map is now an explicit fail-closed EdgeError::internal.

MEDIUM — between_bytes_timeout direction: dropped the false write-side claim per Fastly's public Backend API docs (receive-side only). Streamed-upload host-write phase downgraded to BestEffort; cooperative inter-chunk check is the only adapter-side bound.

MEDIUM — Streamed-upload response overshoot: tightened from per-chunk accumulator to closed-form bound — first_byte_ms (headers wait) + one between_bytes_timeout (worst-case first-body-chunk read), one-shot. Footnote 1 single-send section and new §5.4 test row added.

LOW — App-facing consuming body accessor: added OutboundResponse::into_body(self) -> Body. send_all rustdoc now recommends single send + futures::join_all + into_body() as the canonical streamed-response orchestration path on Axum/CF/Spin.

Appendix AY records all five resolutions. Status bumped to rounds 1–50.

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.
@aram356

aram356 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

Round 51 — round-50 carry-overs + Spin WASI write mechanics (commit 8df9d80)

Five follow-up findings from the round-50 reviewer pass. All verified against the cited external docs before edit.

HIGH — Fastly dynamic-backend semantics contradictory across sections. Round 50 fixed the §4.3 step-5 algorithm but the earlier Dynamic backends introductory paragraph still preserved the false "identical name + identical properties re-registers / returns Ok" carve-out. Rewritten in place: session-uniqueness is unconditional per the SDK; EdgeZero owns the entire uniqueness story at the guest layer via the adapter-local cache; NameInUse outside that cache is fail-closed EdgeError::internal. Two historical Appendix-AK entries marked "Superseded by Appendix AY."

MEDIUM — Fastly buffered-fallback named an unavailable cap; §5.4 still bundled Fastly with CF/Spin. Added FASTLY_RESPONSE_STREAM_BUFFER_BYTES (default 16 MiB, mirrors AXUM_RESPONSE_STREAM_BUFFER_BYTES). §5.4 lazy-passthrough rows split: (a) CF/Spin yield-first-bytes row excludes Fastly; (b) CF/Spin mid-stream abort row excludes Fastly; (c) new Axum-and-Fastly buffered-fallback row with both adapter constants named.

MEDIUM — Residual between_bytes_timeout write-side claims. The "stalled streamed-upload mechanics differ per adapter" §5.4 row and §8 risk 7 both still claimed the Fastly between-bytes-timeout bounded guest-to-origin writes. Scrubbed both — Fastly's write phase is BestEffort uniformly. §8 risk 7 retitled and rewritten to track the symmetric "if Fastly adds a documented guest-write timeout in the future" follow-up.

MEDIUM — Spin host-write race mechanically wrong vs WASI. Old wording raced each OutgoingBody::write against a timer; WASI output-stream is nonblocking + readiness-polled. Rewritten as the four-step pattern: subscribe() pollable → futures::select! pollable-ready vs monotonic-clock timer → on timer-win drop the handle + return gateway_timeout → on pollable-win call nonblocking check_write() for permitted byte count + write() within that bound, looping. §5.4 row updated to match.

LOW — Typo: docsare migrated → docs are migrated in §1.3.

Appendix AZ records all five resolutions; status bumped to rounds 1–51.

Base automatically changed from feature/extensible-cli to main June 29, 2026 21:20
@aram356 aram356 self-assigned this Jul 2, 2026
aram356 added 17 commits July 2, 2026 14:45
# 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.
…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.
@aram356 aram356 mentioned this pull request Oct 28, 2026
5 tasks
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.

This branch has not been deployed

No deployments
Sign up for free to 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.

Implement portable outbound HTTP and request lifecycle contracts

2 participants