feat: provision --local (umbrella PR)#287
Draft
aram356 wants to merge 30 commits into
Draft
Conversation
Empty tracking commit for the implementation work tracked in: docs/superpowers/plans/2026-06-27-provision-local.md Issues: - Epic: <epic-issue-url> - Per-section sub-issues linked from the epic. This PR opens as a DRAFT and stays draft until Section 1 lands its first real commit. Each section opens as its own follow-up PR that lands here before the umbrella merges to main.
This was referenced Jun 29, 2026
run_shared_checks iterates every declared adapter and dispatches validate_adapter_manifest, which for Spin does fs::read_to_string(manifest_root.join(rel)). With the containment guard sitting after run_shared_checks, a manifest declaring [adapters.spin.adapter].manifest = "../outside/spin.toml" could trigger a filesystem read outside the project root before the guard rejected it — a spec violation of §"Path containment (MUST)" which requires the helper run BEFORE any manifest-path use. Fix by relocating the check to fire immediately after load_push_context, and looping over every declared adapter (not just ctx.adapter) since run_shared_checks reads all of them. Also close Task 7's Minor about the duplicate adapter_entry call by removing the now-redundant per-adapter guard block. Regression test: config_push_local_rejects_parent_traversal_in_ sibling_spin_adapter declares a poisoned Spin adapter alongside the pushed axum adapter, and asserts the error names the containment violation (not Spin's "failed to read spin manifest" message that would surface under the old ordering). Also tighten copy_tree's else-branch to explicitly gate on is_regular_file() rather than "everything non-dir non-symlink", add a Unix symlink-skip test, and drop a stale #[expect(dead_code)] on ValidationContext::manifest_path that now has real callers.
…to feature/provision-local-impl
The bare-cwd variant of the accept-test (--manifest edgezero.toml)
previously wrote the manifest to a tempdir and set EDGEZERO_MANIFEST,
but run_provision reads args.manifest directly (no env fallback).
The test therefore failed on manifest load ("failed to load
edgezero.toml") and its negative !contains(path-safety-markers)
assertion vacuously passed — no actual coverage of the
`args.manifest.parent() == ""` fallback.
Fix by adding a CwdGuard RAII helper that chdirs into the tempdir
under the manifest_guard() serialisation lock and restores the
previous cwd on drop. Both accept-tests now also assert positively
that the error is the (true, true) dispatch stub ("local dry-run
staging lands in Task 10/11"), proving the manifest loaded AND
path-safety passed AND we reached the dispatch matrix. Drop the
now-unnecessary EnvOverride from both tests.
Reviewer: reviewer of Task 9 pushed this as a Low ahead of Task 10
because run_with_staging depends on manifest-root/cwd correctness.
…nored until Section 5)
The deployed block is parsed as a shared schema — the manifest parser
never branches on the adapter name inside `[adapters.<name>.deployed]`.
Field membership (kv_namespaces / preview_kv_namespaces / service_id)
reflects which adapters happen to use which subset, but that mapping
lives in edgezero-adapter (Section 6's deployed_state_for), not core.
Tests name the fixture adapter `demo` and describe the field kind
they capture ("captures kv_namespace_maps", "captures service_id")
rather than the adapter that consumes it, so a future reader doesn't
mistake the parser for adapter-aware.
Spec §"Dry-run" step 3: the tempdir IS the dry-run mechanism. Adapters take their real-write branches against the staged tree so operators can preview the actual files that would land. My Task 11 brief incorrectly said hardcode `true` in the (true, true) arm; a review caught it before Section 5 landed real writers that would have hit the wrong branches. Adds provision_local_dry_run_passes_dry_run_false_to_adapter using the existing FakeBootstrapAdapter + RECORDED_DRY_RUN observer.
Docstring-only cleanups flagged by Task 15 review: - FakeBootstrapAdapter's #[expect(missing_trait_methods)] reason listed the four originally-overridden methods (name / provision / synthesise_baseline_manifest / validate_adapter_manifest) but not the newly-added deployed_fields override. Refreshed the list. - NoFieldsFakeAdapter's reason previously said "every other trait method inherits its default (no-op or Unsupported)" without distinguishing the three required-but-implemented methods from the actual inherited defaults. Rewrote to name the required overrides explicitly and spell out that deployed_fields's `&[]` default is the intent this fake exercises. - validate_deployed_field_ownership doc comment now records the case-handling invariant (adapter_registry::get_adapter normalises via to_ascii_lowercase, so [adapters.Fastly.deployed] resolves to the same registered adapter as [adapters.fastly.deployed]) so a future reader doesn't have to re-derive it from the registry.
…p into run_provision Two blocking review findings: - deployed_state_for was left as a `None`-returning stub through the whole of Section 4. Real deployed IDs in [adapters.<name>.deployed] never reached the adapter's synthesise_baseline_manifest call — the main Section 4 promise (teammates' `provision --local` regenerates local manifests from tracked durable IDs) was broken. Translator now maps service_id → state.fields, kv_namespaces + preview_kv_namespaces → state.sub_tables. Returns None only when every field is empty (matches pre-Task-14 signal for empty state). - validate_deployed_field_ownership was wired into run_shared_checks (config validate / push / diff) but NOT into run_provision. Gap let `edgezero provision` accept deployed blocks that `config validate` correctly rejected. Now called from a new run_manifest_shape_gates helper that also holds the existing capability + handler-path checks; run_provision drops from 101 to 90 lines and stays under the workspace too_many_lines lint. Four new tests: deployed_state_for_translates_all_field_kinds, deployed_state_for_returns_none_when_all_fields_empty, provision_local_threads_deployed_state_into_synthesiser (extends the existing fake with a RECORDED_SYNTH_DEPLOYED observer), provision_rejects_deployed_block_with_field_adapter_does_not_own.
Category B (deleted, redundant with fake-based Task 11 coverage): - run_provision_axum_dry_run_is_also_a_no_op - run_provision_spin_dry_run_dispatches_to_adapter - run_provision_cloudflare_dry_run_dispatches_to_adapter - run_provision_fastly_dry_run_dispatches_to_adapter All four asserted only "run_provision reaches the adapter with the requested dry_run value." That's strictly weaker than provision_cloud_dry_run_passes_dry_run_true_to_adapter (which observes RECORDED_DRY_RUN on the fake) and the corresponding local matrix tests. Delegating "does this adapter's dry-run branch run" to the adapter's own crate tests where relevant. Category A (renamed + docstring): - run_provision_axum_prints_local_only_notes_for_each_store → run_provision_cloud_non_dry_run_succeeds_when_adapter_is_side _effect_free. The old name promised assertions the test never made; the new name says what actually runs. - Four `run_provision_spin_*` tests kept — they exercise CLI logic that requires an adapter with a specific `merged_id_kinds()` shape (capability gate, collision detection, validate_adapter_manifest actually validating). Spin is the illustrative example, and each test's docstring now says so. Test count: 191 → 187 (-4). No coverage loss — every deleted test was subsumed by a fake-based counterpart with stronger observers.
…ed schema check High-1 — local dry-run was double-logging status lines. `run_local_dry_run` rendered a sanitized report (staged tempdir paths swapped back to project-relative) and logged it, then returned the original `outcome`. `run_provision`'s trailing status-line loop would then log the raw staged-tempdir paths again. Section 5's real adapter writers would surface `/var/folders/…` paths on stdout. Fix: return an outcome with `status_lines` cleared — the sanitized report already contains the rewritten status content, so the outer loop becomes a no-op. Full log-capture regression is still deferred to Task 13's ignored test (log::set_logger is one-shot per process). High-2 — `write_baseline_to_disk` bypassed path containment. The manifest-declared paths are gated by `assert_provision_paths _contained`, but the *baseline pairs* returned by an adapter's `synthesise_baseline_manifest` weren't. A synthesiser returning `/tmp/x.toml` or `../../etc/passwd` would escape via `root.join()` (absolute) or ordinary path resolution (parent-dir components). Fix: reject absolute paths + `Component::ParentDir` before joining. Two regression tests: `write_baseline_rejects_absolute_path`, `write_baseline_rejects_parent_traversal`. Medium-3 — `merge_deployed_into_manifest` trusted adapter output blindly. Existing manifests are gated by `validate_deployed_field_ownership` before dispatch, but the writeback itself had no schema check — a buggy adapter emitting an unknown key (e.g. `nonsense_key`) or a known key it doesn't own (Cloudflare emitting `service_id`) would persist into edgezero.toml and break future loads via `deny_unknown_fields` on `ManifestAdapterDeployed`. Fix: extend `merge_deployed_into_manifest` signature with `owned_fields: &[&str]`; check every key in `state.fields` / `state.sub_tables` against a hard-coded schema list + the owned fields. Two regression tests: `merge_deployed_rejects_adapter_emitted_unknown_field`, `merge_deployed_rejects_adapter_emitted_non_owned_field`. Call sites updated: `run_provision` passes `adapter.deployed_fields()`; the three pre-existing merge_deployed tests pass an appropriate superset.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Umbrella PR for the
provision --localworkstream.Closes #288.
Source artifacts
docs/superpowers/specs/2026-06-23-provision-local.md(signed off by external review)docs/superpowers/plans/2026-06-27-provision-local.md— 43 tasks across 9 sections, also signed offArchitecture
A new
ProvisionMode::Localarm threads throughAdapter::provision. Local mode:toml_edit::DocumentMut(CLI-owned bootstrap, before validation).edgezero/.env,.dev.vars,<spin_crate>/.env)Dry-run stages a real
fs::copyinto atempfile::TempDirand diffs the result back. Cloudflare/Fastly/Spin manifests become gitignored generated state; Axum'saxum.tomlstays tracked. Generated<app-cli>runsrun_provision_typed::<C>to add#[secret]-field placeholders the bundlededgezerocan't see.Execution plan
This umbrella PR opens as draft and stays draft. Each plan section gets its own follow-up PR that merges into this branch; once Sections 1–9 are all green, this PR converts to ready-for-review and merges to
main.Section breakdown
Each section tracks one sub-issue:
ManifestAdapterDeployedschema + writeback (Tasks 14–16, 16b–16c) — Section 4 — ManifestAdapterDeployed schema + writeback (Tasks 14–16, 16b–16c) #292CI gates (from the plan's Global Constraints)
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace --all-targetscargo check --workspace --all-targets --features "fastly cloudflare spin"cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spinTest plan
scripts/smoke_test_{config,kv,secrets,config_key_override}.sh) pass with warm-upprovision_local_*cases + Spin's env-label alignment quartet (Section 9)