Repository: bytecodealliance/wasmtime
Pull request #9025 (2024-07-26 18:51:03)

Enforce `clippy::clone_on_copy` for the workspace
(Builds on top of https://github.com/bytecodealliance/wasmtime/pull/9024)


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index ada3daffb..c799f98f2 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -197,6 +197,7 @@ jobs:
       audit: ${{ steps.calculate.outputs.audit }}
       preview1-adapter: ${{ steps.calculate.outputs.preview1-adapter }}
       run-dwarf: ${{ steps.calculate.outputs.run-dwarf }}
+      run-wasi-keyvalue: ${{ steps.calculate.outputs.run-wasi-keyvalue }}
     steps:
     - uses: actions/checkout@v4
     - id: calculate
@@ -241,6 +242,9 @@ jobs:
           if grep -q debug names.log; then
             echo run-dwarf=true >> $GITHUB_OUTPUT
           fi
+          if grep -q wasi-keyvalue names.log; then
+            echo run-wasi-keyvalue=true >> $GITHUB_OUTPUT
+          fi
         fi
         matrix="$(node ./ci/build-test-matrix.js ./commits.log ./names.log $run_full)"
         echo "test-matrix={\"include\":$(echo $matrix)}" >> $GITHUB_OUTPUT
@@ -256,6 +260,7 @@ jobs:
             echo audit=true >> $GITHUB_OUTPUT
             echo preview1-adapter=true >> $GITHUB_OUTPUT
             echo run-dwarf=true >> $GITHUB_OUTPUT
+            echo run-wasi-keyvalue=true >> $GITHUB_OUTPUT
         fi
 
   # Build all documentation of Wasmtime, including the C API documentation,
@@ -737,6 +742,44 @@ jobs:
       env:
         GH_TOKEN: ${{ github.token }}
 
+  # Test the `wasmtime-wasi-keyvalue` crate. Split out from the main tests
+  # because it needs additional database service.
+  test_wasi_keyvalue:
+    name: Test wasi-keyvalue
+    runs-on: ubuntu-latest
+    needs: determine
+    if: needs.determine.outputs.run-wasi-keyvalue
+    # Setup redis server
+    services:
+      redis:
+        # Docker Hub image
+        image: redis
+        # Set health checks to wait until redis has started
+        options: >-
+          --health-cmd "redis-cli ping"
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+        ports:
+          # Maps port 6379 on service container to the host
+          - 6379:6379
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        submodules: true
+    - uses: ./.github/actions/install-rust
+    # Install Rust targets
+    - run: rustup target add wasm32-wasi
+    - run: cargo test --all-features -p wasmtime-wasi-keyvalue
+      env:
+        RUST_BACKTRACE: 1
+
+    # common logic to cancel the entire run if this job fails
+    - run: gh run cancel ${{ github.run_id }}
+      if: failure() && github.event_name != 'pull_request'
+      env:
+        GH_TOKEN: ${{ github.token }}
+
   # Test the `wasmtime-fuzzing` crate. Split out from the main tests because
   # `--all-features` brings in OCaml, which is a pain to get setup for all
   # targets.
diff --git a/Cargo.lock b/Cargo.lock
index e24c8de3a..8f91d5486 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -477,6 +477,20 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
 [[package]]
 name = "component-fuzz-util"
 version = "0.0.0"
@@ -2233,6 +2247,27 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "redis"
+version = "0.25.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "combine",
+ "futures-util",
+ "itoa",
+ "percent-encoding",
+ "pin-project-lite",
+ "ryu",
+ "sha1_smol",
+ "socket2 0.5.7",
+ "tokio",
+ "tokio-util",
+ "url",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.2.13"
@@ -2492,6 +2527,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "sha1_smol"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
+
 [[package]]
 name = "sha2"
 version = "0.10.2"
@@ -2582,6 +2623,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "souper-ir"
 version = "2.1.0"
@@ -2843,7 +2894,7 @@ dependencies = [
  "num_cpus",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2",
+ "socket2 0.4.9",
  "tokio-macros",
  "windows-sys 0.48.0",
 ]
@@ -3567,6 +3618,7 @@ dependencies = [
  "wasmtime-test-macros",
  "wasmtime-wasi",
  "wasmtime-wasi-http",
+ "wasmtime-wasi-keyvalue",
  "wasmtime-wasi-nn",
  "wasmtime-wasi-runtime-config",
  "wasmtime-wasi-threads",
@@ -3879,6 +3931,20 @@ dependencies = [
  "webpki-roots",
 ]
 
+[[package]]
+name = "wasmtime-wasi-keyvalue"
+version = "24.0.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "redis",
+ "test-programs-artifacts",
+ "tokio",
+ "url",
+ "wasmtime",
+ "wasmtime-wasi",
+]
+
 [[package]]
 name = "wasmtime-wasi-nn"
 version = "24.0.0"
diff --git a/Cargo.toml b/Cargo.toml
index ffd2b93a2..f16d74958 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -54,6 +54,7 @@ wasi-common = { workspace = true, default-features = true, features = ["exit"],
 wasmtime-wasi = { workspace = true, default-features = true, optional = true }
 wasmtime-wasi-nn = { workspace = true, optional = true }
 wasmtime-wasi-runtime-config = { workspace = true, optional = true }
+wasmtime-wasi-keyvalue = { workspace = true, optional = true }
 wasmtime-wasi-threads = { workspace = true, optional = true }
 wasmtime-wasi-http = { workspace = true, optional = true }
 clap = { workspace = true }
@@ -174,7 +175,8 @@ unused-lifetimes = 'warn'
 # The default set of lints in Clippy is viewed as "too noisy" right now so
 # they're all turned off by default. Selective lints are then enabled below as
 # necessary.
-all = 'allow'
+all = { level = 'allow', priority = -1 }
+clone_on_copy = 'deny'
 
 [workspace.dependencies]
 arbitrary = { version = "1.3.1" }
@@ -195,6 +197,7 @@ wasmtime-wasi = { path = "crates/wasi", version = "24.0.0", default-features = f
 wasmtime-wasi-http = { path = "crates/wasi-http", version = "=24.0.0", default-features = false }
 wasmtime-wasi-nn = { path = "crates/wasi-nn", version = "24.0.0" }
 wasmtime-wasi-runtime-config = { path = "crates/wasi-runtime-config", version = "24.0.0" }
+wasmtime-wasi-keyvalue = { path = "crates/wasi-keyvalue", version = "24.0.0" }
 wasmtime-wasi-threads = { path = "crates/wasi-threads", version = "24.0.0" }
 wasmtime-component-util = { path = "crates/component-util", version = "=24.0.0" }
 wasmtime-component-macro = { path = "crates/component-macro", version = "=24.0.0" }
@@ -326,6 +329,7 @@ criterion = { version = "0.5.0", default-features = false, features = ["html_rep
 rustc-hash = "1.1.0"
 libtest-mimic = "0.7.0"
 semver = { version = "1.0.17", default-features = false }
+redis = "0.25.4"
 
 # =============================================================================
 #
diff --git a/ci/vendor-wit.sh b/ci/vendor-wit.sh
index 374d9c10f..005f4d3e0 100755
--- a/ci/vendor-wit.sh
+++ b/ci/vendor-wit.sh
@@ -58,6 +58,8 @@ make_vendor "wasi-http" "
 
 make_vendor "wasi-runtime-config" "runtime-config@c667fe6"
 
+make_vendor "wasi-keyvalue" "keyvalue@219ea36"
+
 rm -rf $cache_dir
 
 # Separately (for now), vendor the `wasi-nn` WIT files since their retrieval is
diff --git a/cranelift/codegen/src/alias_analysis.rs b/cranelift/codegen/src/alias_analysis.rs
index 636c0a8e9..6a6e9f274 100644
--- a/cranelift/codegen/src/alias_analysis.rs
+++ b/cranelift/codegen/src/alias_analysis.rs
@@ -212,11 +212,10 @@ impl<'a> AliasAnalysis<'a> {
 
         while let Some(block) = queue.pop() {
             queue_set.remove(&block);
-            let mut state = self
+            let mut state = *self
                 .block_input
                 .entry(block)
-                .or_insert_with(|| LastStores::default())
-                .clone();
+                .or_insert_with(|| LastStores::default());
 
             trace!(
                 "alias analysis: input to block{} is {:?}",
@@ -233,12 +232,12 @@ impl<'a> AliasAnalysis<'a> {
                 let succ_first_inst = func.layout.block_insts(succ).into_iter().next().unwrap();
                 let updated = match self.block_input.get_mut(&succ) {
                     Some(succ_state) => {
-                        let old = succ_state.clone();
+                        let old = *succ_state;
                         succ_state.meet_from(&state, succ_first_inst);
                         *succ_state != old
                     }
                     None => {
-                        self.block_input.insert(succ, state.clone());
+                        self.block_input.insert(succ, state);
                         true
                     }
                 };
diff --git a/cranelift/codegen/src/egraph.rs b/cranelift/codegen/src/egraph.rs
index 2e48d3a78..6225c73e0 100644
--- a/cranelift/codegen/src/egraph.rs
+++ b/cranelift/codegen/src/egraph.rs
@@ -432,7 +432,7 @@ where
             let ty = self.func.dfg.ctrl_typevar(inst);
             match self
                 .effectful_gvn_map
-                .entry((ty, self.func.dfg.insts[inst].clone()))
+                .entry((ty, self.func.dfg.insts[inst]))
             {
                 ScopedEntry::Occupied(o) => {
                     let orig_result = *o.get();
diff --git a/cranelift/codegen/src/ir/dfg.rs b/cranelift/codegen/src/ir/dfg.rs
index 43d0af638..eb2d53265 100644
--- a/cranelift/codegen/src/ir/dfg.rs
+++ b/cranelift/codegen/src/ir/dfg.rs
@@ -1028,7 +1028,7 @@ impl DataFlowGraph {
     /// returning them.
     pub fn clone_inst(&mut self, inst: Inst) -> Inst {
         // First, add a clone of the InstructionData.
-        let inst_data = self.insts[inst].clone();
+        let inst_data = self.insts[inst];
         // If the `inst_data` has a reference to a ValueList, clone it
         // as well, because we can't share these (otherwise mutating
         // one would affect the other).
diff --git a/cranelift/codegen/src/isa/aarch64/inst/emit.rs b/cranelift/codegen/src/isa/aarch64/inst/emit.rs
index de63d7cef..00c7bb473 100644
--- a/cranelift/codegen/src/isa/aarch64/inst/emit.rs
+++ b/cranelift/codegen/src/isa/aarch64/inst/emit.rs
@@ -847,7 +847,7 @@ impl MachInstEmit for Inst {
                     _ => unimplemented!("{:?}", alu_op),
                 };
                 let top9 = top9 | size.sf_bit() << 8;
-                let imml = if inv { imml.invert() } else { imml.clone() };
+                let imml = if inv { imml.invert() } else { *imml };
                 sink.put4(enc_arith_rr_imml(top9, imml.enc_bits(), rn, rd));
             }
 
diff --git a/cranelift/codegen/src/isa/aarch64/lower/isle/generated_code.rs b/cranelift/codegen/src/isa/aarch64/lower/isle/generated_code.rs
index f00fd2f86..54bfd8c27 100644
--- a/cranelift/codegen/src/isa/aarch64/lower/isle/generated_code.rs
+++ b/cranelift/codegen/src/isa/aarch64/lower/isle/generated_code.rs
@@ -4,6 +4,11 @@
 // mod generated_code;` trick either.
 #![allow(missing_docs, dead_code, unreachable_code, unreachable_patterns)]
 #![allow(unused_imports, unused_variables, non_snake_case, unused_mut)]
-#![allow(irrefutable_let_patterns, unused_assignments, non_camel_case_types)]
+#![allow(
+    irrefutable_let_patterns,
+    unused_assignments,
+    non_camel_case_types,
+    clippy::clone_on_copy
+)]
 
 include!(concat!(env!("ISLE_DIR"), "/isle_aarch64.rs"));
diff --git a/cranelift/codegen/src/isa/riscv64/inst/emit.rs b/cranelift/codegen/src/isa/riscv64/inst/emit.rs
index ea279b4f4..d48f49a1e 100644
--- a/cranelift/codegen/src/isa/riscv64/inst/emit.rs
+++ b/cranelift/codegen/src/isa/riscv64/inst/emit.rs
@@ -232,11 +232,11 @@ impl MachInstEmit for Inst {
     fn emit(&self, sink: &mut MachBuffer<Inst>, emit_info: &Self::Info, state: &mut EmitState) {
         // Check if we need to update the vector state before emitting this instruction
         if let Some(expected) = self.expected_vstate() {
-            if state.vstate != EmitVState::Known(expected.clone()) {
+            if state.vstate != EmitVState::Known(*expected) {
                 // Update the vector state.
                 Inst::VecSetState {
                     rd: writable_zero_reg(),
-                    vstate: expected.clone(),
+                    vstate: *expected,
                 }
                 .emit(sink, emit_info, state);
             }
@@ -2532,7 +2532,7 @@ impl Inst {
                 ));
 
                 // Update the current vector emit state.
-                state.vstate = EmitVState::Known(vstate.clone());
+                state.vstate = EmitVState::Known(*vstate);
             }
 
             &Inst::VecLoad {
@@ -2557,7 +2557,7 @@ impl Inst {
                             let tmp = writable_spilltmp_reg();
                             Inst::LoadAddr {
                                 rd: tmp,
-                                mem: base.clone(),
+                                mem: *base,
                             }
                             .emit(sink, emit_info, state);
                             tmp.to_reg()
@@ -2604,7 +2604,7 @@ impl Inst {
                             let tmp = writable_spilltmp_reg();
                             Inst::LoadAddr {
                                 rd: tmp,
-                                mem: base.clone(),
+                                mem: *base,
                             }
                             .emit(sink, emit_info, state);
                             tmp.to_reg()
diff --git a/cranelift/codegen/src/isa/riscv64/inst/mod.rs b/cranelift/codegen/src/isa/riscv64/inst/mod.rs
index 80bd149b7..73dae4fd1 100644
--- a/cranelift/codegen/src/isa/riscv64/inst/mod.rs
+++ b/cranelift/codegen/src/isa/riscv64/inst/mod.rs
@@ -739,7 +739,7 @@ impl MachInst for Inst {
 
     fn is_move(&self) -> Option<(Writable<Reg>, Reg)> {
         match self {
-            Inst::Mov { rd, rm, .. } => Some((rd.clone(), rm.clone())),
+            Inst::Mov { rd, rm, .. } => Some((*rd, *rm)),
             _ => None,
         }
     }
@@ -916,7 +916,7 @@ impl Inst {
                 String::default()
             };
             regs.iter().for_each(|i| {
-                x.push_str(format_reg(i.clone()).as_str());
+                x.push_str(format_reg(*i).as_str());
                 if *i != *regs.last().unwrap() {
                     x.push_str(",");
                 }
diff --git a/cranelift/codegen/src/isa/riscv64/lower/isle/generated_code.rs b/cranelift/codegen/src/isa/riscv64/lower/isle/generated_code.rs
index c595c311d..d5d1feae9 100644
--- a/cranelift/codegen/src/isa/riscv64/lower/isle/generated_code.rs
+++ b/cranelift/codegen/src/isa/riscv64/lower/isle/generated_code.rs
@@ -4,6 +4,6 @@
 // mod generated_code;` trick either.
 #![allow(dead_code, unreachable_code, unreachable_patterns)]
 #![allow(unused_imports, unused_variables, non_snake_case, unused_mut)]
-#![allow(irrefutable_let_patterns)]
+#![allow(irrefutable_let_patterns, clippy::clone_on_copy)]
 
 include!(concat!(env!("ISLE_DIR"), "/isle_riscv64.rs"));
diff --git a/cranelift/codegen/src/isa/s390x/inst/mod.rs b/cranelift/codegen/src/isa/s390x/inst/mod.rs
index 05a36e098..c38014513 100644
--- a/cranelift/codegen/src/isa/s390x/inst/mod.rs
+++ b/cranelift/codegen/src/isa/s390x/inst/mod.rs
@@ -892,7 +892,7 @@ fn s390x_get_operands(inst: &mut Inst, collector: &mut DenyReuseVisitor<impl Ope
             for CallArgPair { vreg, preg } in uses {
                 collector.reg_fixed_use(vreg, *preg);
             }
-            let mut clobbers = clobbers.clone();
+            let mut clobbers = *clobbers;
             clobbers.add(link.to_reg().to_real_reg().unwrap().into());
             for CallRetPair { vreg, preg } in defs {
                 clobbers.remove(PReg::from(preg.to_real_reg().unwrap()));
@@ -912,7 +912,7 @@ fn s390x_get_operands(inst: &mut Inst, collector: &mut DenyReuseVisitor<impl Ope
             for CallArgPair { vreg, preg } in uses {
                 collector.reg_fixed_use(vreg, *preg);
             }
-            let mut clobbers = clobbers.clone();
+            let mut clobbers = *clobbers;
             clobbers.add(link.to_reg().to_real_reg().unwrap().into());
             for CallRetPair { vreg, preg } in defs {
                 clobbers.remove(PReg::from(preg.to_real_reg().unwrap()));
diff --git a/cranelift/codegen/src/isa/s390x/lower/isle/generated_code.rs b/cranelift/codegen/src/isa/s390x/lower/isle/generated_code.rs
index d1f8767e9..c470bfdad 100644
--- a/cranelift/codegen/src/isa/s390x/lower/isle/generated_code.rs
+++ b/cranelift/codegen/src/isa/s390x/lower/isle/generated_code.rs
@@ -4,6 +4,11 @@
 // mod generated_code;` trick either.
 #![allow(dead_code, unreachable_code, unreachable_patterns)]
 #![allow(unused_imports, unused_variables, non_snake_case, unused_mut)]
-#![allow(irrefutable_let_patterns, unused_assignments, non_camel_case_types)]
+#![allow(
+    irrefutable_let_patterns,
+    unused_assignments,
+    non_camel_case_types,
+    clippy::clone_on_copy
+)]
 
 include!(concat!(env!("ISLE_DIR"), "/isle_s390x.rs"));
diff --git a/cranelift/codegen/src/isa/x64/inst/emit.rs b/cranelift/codegen/src/isa/x64/inst/emit.rs
index 45ae37c96..dfc8e4fe7 100644
--- a/cranelift/codegen/src/isa/x64/inst/emit.rs
+++ b/cranelift/codegen/src/isa/x64/inst/emit.rs
@@ -1416,7 +1416,7 @@ pub(crate) fn emit(
             let alternative = alternative.to_reg();
             let dst = dst.to_writable_reg();
             debug_assert_eq!(alternative, dst.to_reg());
-            let consequent = consequent.clone().to_reg();
+            let consequent = consequent.to_reg();
 
             // Lowering of the Select IR opcode when the input is an fcmp relies on the fact that
             // this doesn't clobber flags. Make sure to not do so here.
diff --git a/cranelift/codegen/src/isa/x64/lower/isle.rs b/cranelift/codegen/src/isa/x64/lower/isle.rs
index f9f0e9182..b65c6fe74 100644
--- a/cranelift/codegen/src/isa/x64/lower/isle.rs
+++ b/cranelift/codegen/src/isa/x64/lower/isle.rs
@@ -691,7 +691,7 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
             self.lower_ctx,
             &self.backend.flags,
             &self.backend.triple,
-            libcall.clone(),
+            *libcall,
             &[a],
         )
         .expect("Failed to emit LibCall");
@@ -706,7 +706,7 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
             self.lower_ctx,
             &self.backend.flags,
             &self.backend.triple,
-            libcall.clone(),
+            *libcall,
             &[a, b],
         )
         .expect("Failed to emit LibCall");
@@ -721,7 +721,7 @@ impl Context for IsleContext<'_, '_, MInst, X64Backend> {
             self.lower_ctx,
             &self.backend.flags,
             &self.backend.triple,
-            libcall.clone(),
+            *libcall,
             &[a, b, c],
         )
         .expect("Failed to emit LibCall");
diff --git a/cranelift/codegen/src/isa/x64/lower/isle/generated_code.rs b/cranelift/codegen/src/isa/x64/lower/isle/generated_code.rs
index a03aed0a5..49ec3dc4c 100644
--- a/cranelift/codegen/src/isa/x64/lower/isle/generated_code.rs
+++ b/cranelift/codegen/src/isa/x64/lower/isle/generated_code.rs
@@ -8,7 +8,8 @@
     irrefutable_let_patterns,
     unused_assignments,
     non_camel_case_types,
-    missing_docs
+    missing_docs,
+    clippy::clone_on_copy
 )]
 
 include!(concat!(env!("ISLE_DIR"), "/isle_x64.rs"));
diff --git a/cranelift/codegen/src/machinst/blockorder.rs b/cranelift/codegen/src/machinst/blockorder.rs
index fb4d56760..b31a5a1ba 100644
--- a/cranelift/codegen/src/machinst/blockorder.rs
+++ b/cranelift/codegen/src/machinst/blockorder.rs
@@ -306,7 +306,7 @@ impl BlockLoweringOrder {
     /// Get the successor indices for a lowered block.
     pub fn succ_indices(&self, block: BlockIndex) -> (Option<Inst>, &[BlockIndex]) {
         let (opt_inst, range) = &self.lowered_succ_ranges[block.index()];
-        (opt_inst.clone(), &self.lowered_succ_indices[range.clone()])
+        (*opt_inst, &self.lowered_succ_indices[range.clone()])
     }
 
     /// Determine whether the given lowered-block index is cold.
diff --git a/cranelift/codegen/src/machinst/lower.rs b/cranelift/codegen/src/machinst/lower.rs
index 32db947a2..fa6e8265d 100644
--- a/cranelift/codegen/src/machinst/lower.rs
+++ b/cranelift/codegen/src/machinst/lower.rs
@@ -654,7 +654,7 @@ impl<'func, I: VCodeInst> Lower<'func, I> {
             .enumerate()
         {
             let regs = if ret.purpose == ArgumentPurpose::StructReturn {
-                self.sret_reg.unwrap().clone()
+                self.sret_reg.unwrap()
             } else {
                 rets.next().unwrap()
             };
diff --git a/cranelift/codegen/src/opts.rs b/cranelift/codegen/src/opts.rs
index 44ef9e6b5..c22a0ab2e 100644
--- a/cranelift/codegen/src/opts.rs
+++ b/cranelift/codegen/src/opts.rs
@@ -97,7 +97,7 @@ where
                 ValueDef::Result(inst, _) if ctx.ctx.func.dfg.inst_results(inst).len() == 1 => {
                     let ty = ctx.ctx.func.dfg.value_type(value);
                     trace!(" -> value of type {}", ty);
-                    return Some((ty, ctx.ctx.func.dfg.insts[inst].clone()));
+                    return Some((ty, ctx.ctx.func.dfg.insts[inst]));
                 }
                 _ => {}
             }
@@ -196,9 +196,7 @@ impl<'a, 'b, 'c> generated_code::Context for IsleContext<'a, 'b, 'c> {
     }
 
     fn make_inst_ctor(&mut self, ty: Type, op: &InstructionData) -> Value {
-        let value = self
-            .ctx
-            .insert_pure_enode(NewOrExistingInst::New(op.clone(), ty));
+        let value = self.ctx.insert_pure_enode(NewOrExistingInst::New(*op, ty));
         trace!("make_inst_ctor: {:?} -> {}", op, value);
         value
     }
diff --git a/cranelift/codegen/src/opts/generated_code.rs b/cranelift/codegen/src/opts/generated_code.rs
index b196e1050..cd2362988 100644
--- a/cranelift/codegen/src/opts/generated_code.rs
+++ b/cranelift/codegen/src/opts/generated_code.rs
@@ -6,6 +6,6 @@
 // mod generated_code;` trick either.
 #![allow(dead_code, unreachable_code, unreachable_patterns)]
 #![allow(unused_imports, unused_variables, non_snake_case, unused_mut)]
-#![allow(irrefutable_let_patterns, non_camel_case_types)]
+#![allow(irrefutable_let_patterns, non_camel_case_types, clippy::clone_on_copy)]
 
 include!(concat!(env!("ISLE_DIR"), "/isle_opt.rs"));
diff --git a/cranelift/frontend/src/frontend.rs b/cranelift/frontend/src/frontend.rs
index 5a7936bed..3e10eb7bb 100644
--- a/cranelift/frontend/src/frontend.rs
+++ b/cranelift/frontend/src/frontend.rs
@@ -119,7 +119,7 @@ impl<'short, 'long> InstBuilderBase<'short> for FuncInstBuilder<'short, 'long> {
         // We only insert the Block in the layout when an instruction is added to it
         self.builder.ensure_inserted_block();
 
-        let inst = self.builder.func.dfg.make_inst(data.clone());
+        let inst = self.builder.func.dfg.make_inst(data);
         self.builder.func.dfg.make_inst_results(inst, ctrl_typevar);
         self.builder.func.layout.append_inst(inst, self.block);
         if !self.builder.srcloc.is_default() {
diff --git a/cranelift/interpreter/src/instruction.rs b/cranelift/interpreter/src/instruction.rs
index 993584ce1..b3c12997c 100644
--- a/cranelift/interpreter/src/instruction.rs
+++ b/cranelift/interpreter/src/instruction.rs
@@ -25,7 +25,7 @@ impl<'a> DfgInstructionContext<'a> {
 
 impl InstructionContext for DfgInstructionContext<'_> {
     fn data(&self) -> InstructionData {
-        self.1.insts[self.0].clone()
+        self.1.insts[self.0]
     }
 
     fn args(&self) -> &[Value] {
diff --git a/cranelift/interpreter/src/step.rs b/cranelift/interpreter/src/step.rs
index 879417e8f..940a16be3 100644
--- a/cranelift/interpreter/src/step.rs
+++ b/cranelift/interpreter/src/step.rs
@@ -90,7 +90,7 @@ where
                     .get_current_function()
                     .dfg
                     .constants
-                    .get(constant_handle.clone());
+                    .get(constant_handle);
                 match (ctrl_ty.bytes(), opcode) {
                     (_, Opcode::F128const) => DataValue::F128(buffer.try_into().expect("a 16-byte data buffer")),
                     (16, Opcode::Vconst) => DataValue::V128(buffer.as_slice().try_into().expect("a 16-byte data buffer")),
diff --git a/cranelift/interpreter/src/value.rs b/cranelift/interpreter/src/value.rs
index 737e6a008..bfb4cace4 100644
--- a/cranelift/interpreter/src/value.rs
+++ b/cranelift/interpreter/src/value.rs
@@ -801,12 +801,11 @@ impl DataValueExt for DataValue {
             DataValue::I128(a) => DataValue::I128(!a),
             DataValue::F32(a) => DataValue::F32(!a),
             DataValue::F64(a) => DataValue::F64(!a),
-            DataValue::V128(a) => {
-                let mut a2 = a.clone();
-                for a in a2.iter_mut() {
-                    *a = !*a;
+            DataValue::V128(mut a) => {
+                for byte in a.iter_mut() {
+                    *byte = !*byte;
                 }
-                DataValue::V128(a2)
+                DataValue::V128(a)
             }
             _ => unimplemented!(),
         })
diff --git a/cranelift/wasm/src/code_translator.rs b/cranelift/wasm/src/code_translator.rs
index 044436aad..8b9fd7532 100644
--- a/cranelift/wasm/src/code_translator.rs
+++ b/cranelift/wasm/src/code_translator.rs
@@ -1689,17 +1689,17 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
         | Operator::V128Store32Lane { memarg, lane }
         | Operator::V128Store64Lane { memarg, lane } => {
             let vector = pop1_with_bitcast(state, type_of(op), builder);
-            state.push1(builder.ins().extractlane(vector, lane.clone()));
+            state.push1(builder.ins().extractlane(vector, *lane));
             translate_store(memarg, ir::Opcode::Store, builder, state, environ)?;
         }
         Operator::I8x16ExtractLaneS { lane } | Operator::I16x8ExtractLaneS { lane } => {
             let vector = pop1_with_bitcast(state, type_of(op), builder);
-            let extracted = builder.ins().extractlane(vector, lane.clone());
+            let extracted = builder.ins().extractlane(vector, *lane);
             state.push1(builder.ins().sextend(I32, extracted))
         }
         Operator::I8x16ExtractLaneU { lane } | Operator::I16x8ExtractLaneU { lane } => {
             let vector = pop1_with_bitcast(state, type_of(op), builder);
-            let extracted = builder.ins().extractlane(vector, lane.clone());
+            let extracted = builder.ins().extractlane(vector, *lane);
             state.push1(builder.ins().uextend(I32, extracted));
             // On x86, PEXTRB zeroes the upper bits of the destination register of extractlane so
             // uextend could be elided; for now, uextend is needed for Cranelift's type checks to
@@ -1710,7 +1710,7 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
         | Operator::F32x4ExtractLane { lane }
         | Operator::F64x2ExtractLane { lane } => {
             let vector = pop1_with_bitcast(state, type_of(op), builder);
-            state.push1(builder.ins().extractlane(vector, lane.clone()))
+            state.push1(builder.ins().extractlane(vector, *lane))
         }
         Operator::I8x16ReplaceLane { lane } | Operator::I16x8ReplaceLane { lane } => {
             let (vector, replacement) = state.pop2();
diff --git a/crates/c-api/src/async.rs b/crates/c-api/src/async.rs
index f98a7a589..2e29b8839 100644
--- a/crates/c-api/src/async.rs
+++ b/crates/c-api/src/async.rs
@@ -233,7 +233,7 @@ async fn do_func_call_async(
     match result {
         Ok(()) => {
             for (slot, val) in results.iter_mut().zip(wt_results.iter()) {
-                crate::initialize(slot, wasmtime_val_t::from_val(&mut store, val.clone()));
+                crate::initialize(slot, wasmtime_val_t::from_val(&mut store, *val));
             }
             params.truncate(0);
             store.as_context_mut().data_mut().wasm_val_storage = params;
diff --git a/crates/c-api/src/func.rs b/crates/c-api/src/func.rs
index 19e45b3df..0e85ee9b9 100644
--- a/crates/c-api/src/func.rs
+++ b/crates/c-api/src/func.rs
@@ -368,7 +368,7 @@ pub unsafe extern "C" fn wasmtime_func_call(
         Ok(Ok(())) => {
             let results = crate::slice_from_raw_parts_mut(results, nresults);
             for (slot, val) in results.iter_mut().zip(wt_results.iter()) {
-                crate::initialize(slot, wasmtime_val_t::from_val(&mut scope, val.clone()));
+                crate::initialize(slot, wasmtime_val_t::from_val(&mut scope, *val));
             }
             params.truncate(0);
             scope.as_context_mut().data_mut().wasm_val_storage = params;
diff --git a/crates/environ/src/component/dfg.rs b/crates/environ/src/component/dfg.rs
index 9804de86a..4766ebdfe 100644
--- a/crates/environ/src/component/dfg.rs
+++ b/crates/environ/src/component/dfg.rs
@@ -706,7 +706,7 @@ impl LinearizeDfg<'_> {
         V: EntityRef,
     {
         if let Some(val) = map(self).get(&key) {
-            return val.clone();
+            return *val;
         }
         let tmp = gen(self, key);
         let index = V::new(map(self).len());
diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs
index 1c471cc1e..85319865b 100644
--- a/crates/environ/src/component/types_builder.rs
+++ b/crates/environ/src/component/types_builder.rs
@@ -352,7 +352,7 @@ impl ComponentTypesBuilder {
                     .into()
             }),
             types::EntityType::Table(ty) => EntityType::Table(self.convert_table_type(ty)?),
-            types::EntityType::Memory(ty) => EntityType::Memory(ty.clone().into()),
+            types::EntityType::Memory(ty) => EntityType::Memory((*ty).into()),
             types::EntityType::Global(ty) => EntityType::Global(self.convert_global_type(ty)),
             types::EntityType::Tag(_) => bail!("exceptions proposal not implemented"),
         })
diff --git a/crates/fuzzing/src/generators/pooling_config.rs b/crates/fuzzing/src/generators/pooling_config.rs
index 2d81e0eb9..0e327bce3 100644
--- a/crates/fuzzing/src/generators/pooling_config.rs
+++ b/crates/fuzzing/src/generators/pooling_config.rs
@@ -114,7 +114,7 @@ impl<'a> Arbitrary<'a> for PoolingAllocationConfig {
             async_stack_zeroing: u.arbitrary()?,
             async_stack_keep_resident: u.int_in_range(0..=1 << 20)?,
 
-            memory_protection_keys: u.choose(&[MpkEnabled::Auto, MpkEnabled::Disable])?.clone(),
+            memory_protection_keys: *u.choose(&[MpkEnabled::Auto, MpkEnabled::Disable])?,
             max_memory_protection_keys: u.int_in_range(0..=20)?,
         })
     }
diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs
index 913e665df..b4ce583e9 100644
--- a/crates/fuzzing/src/oracles.rs
+++ b/crates/fuzzing/src/oracles.rs
@@ -570,7 +570,7 @@ pub fn make_api_calls(api: generators::api::ApiCalls) {
                 let funcs = instance
                     .exports(&mut *store)
                     .filter_map(|e| match e.into_extern() {
-                        Extern::Func(f) => Some(f.clone()),
+                        Extern::Func(f) => Some(f),
                         _ => None,
                     })
                     .collect::<Vec<_>>();
diff --git a/crates/fuzzing/src/oracles/dummy.rs b/crates/fuzzing/src/oracles/dummy.rs
index 75152c399..2530d31d2 100644
--- a/crates/fuzzing/src/oracles/dummy.rs
+++ b/crates/fuzzing/src/oracles/dummy.rs
@@ -31,7 +31,7 @@ pub fn dummy_func<T>(store: &mut Store<T>, ty: FuncType) -> Result<Func> {
     let dummy_results = ty.results().map(dummy_value).collect::<Result<Vec<_>>>()?;
     Ok(Func::new(store, ty.clone(), move |_, _, results| {
         for (slot, dummy) in results.iter_mut().zip(&dummy_results) {
-            *slot = dummy.clone();
+            *slot = *dummy;
         }
         Ok(())
     }))
diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs
index e4fc74c3f..cc163c7b0 100644
--- a/crates/types/src/lib.rs
+++ b/crates/types/src/lib.rs
@@ -236,7 +236,7 @@ impl WasmValType {
             | WasmValType::I64
             | WasmValType::F32
             | WasmValType::F64
-            | WasmValType::V128 => self.clone(),
+            | WasmValType::V128 => *self,
         }
     }
 }
diff --git a/crates/wasi-common/src/snapshots/preview_1.rs b/crates/wasi-common/src/snapshots/preview_1.rs
index 3a022ce5b..74295b8aa 100644
--- a/crates/wasi-common/src/snapshots/preview_1.rs
+++ b/crates/wasi-common/src/snapshots/preview_1.rs
@@ -628,12 +628,11 @@ impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx {
         &mut self,
         memory: &mut GuestMemory<'_>,
         fd: types::Fd,
-        buf: GuestPtr<u8>,
+        mut buf: GuestPtr<u8>,
         buf_len: types::Size,
         cookie: types::Dircookie,
     ) -> Result<types::Size, Error> {
         let mut bufused = 0;
-        let mut buf = buf.clone();
         for entity in self
             .table()
             .get_dir(u32::from(fd))?
diff --git a/crates/wasi-keyvalue/Cargo.toml b/crates/wasi-keyvalue/Cargo.toml
new file mode 100644
index 000000000..417cc126d
--- /dev/null
+++ b/crates/wasi-keyvalue/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "wasmtime-wasi-keyvalue"
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+repository = "https://github.com/bytecodealliance/wasmtime"
+license = "Apache-2.0 WITH LLVM-exception"
+description = "Wasmtime implementation of the wasi-keyvalue API"
+
+[lints]
+workspace = true
+
+[dependencies]
+anyhow = { workspace = true }
+wasmtime = { workspace = true, features = ["runtime", "async", "component-model"] }
+async-trait = { workspace = true }
+url = { workspace = true }
+redis = { workspace = true, optional = true, features = ["tokio-comp"] }
+
+[dev-dependencies]
+test-programs-artifacts = { workspace = true }
+wasmtime-wasi = { workspace = true }
+tokio = { workspace = true, features = ["macros"] }
+
+[features]
+redis = ["dep:redis"]
diff --git a/crates/wasi-keyvalue/src/lib.rs b/crates/wasi-keyvalue/src/lib.rs
new file mode 100644
index 000000000..c1149c370
--- /dev/null
+++ b/crates/wasi-keyvalue/src/lib.rs
@@ -0,0 +1,442 @@
+//! # Wasmtime's [wasi-keyvalue] Implementation
+//!
+//! This crate provides a Wasmtime host implementation of the [wasi-keyvalue]
+//! API. With this crate, the runtime can run components that call APIs in
+//! [wasi-keyvalue] and provide components with access to key-value storages.
+//!
+//! Currently supported storage backends:
+//! * In-Memory (empty identifier)
+//! * Redis, supported identifier format:
+//!   * `redis://[<username>][:<password>@]<hostname>[:port][/<db>]`
+//!   * `redis+unix:///<path>[?db=<db>[&pass=<password>][&user=<username>]]`
+//!
+//! # Examples
+//!
+//! The usage of this crate is very similar to other WASI API implementations
+//! such as [wasi:cli] and [wasi:http].
+//!
+//! A common scenario is accessing redis in a [wasi:cli] component.
+//! A standalone example of doing all this looks like:
+//!
+//! ```
+//! use wasmtime::{
+//!     component::{Linker, ResourceTable},
+//!     Config, Engine, Result, Store,
+//! };
+//! use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};
+//! use wasmtime_wasi_keyvalue::{WasiKeyValue, WasiKeyValueCtx, WasiKeyValueCtxBuilder};
+//!
+//! #[tokio::main]
+//! async fn main() -> Result<()> {
+//!     let mut config = Config::new();
+//!     config.async_support(true);
+//!     let engine = Engine::new(&config)?;
+//!
+//!     let mut store = Store::new(&engine, Ctx {
+//!         table: ResourceTable::new(),
+//!         wasi_ctx: WasiCtxBuilder::new().build(),
+//!         wasi_keyvalue_ctx: WasiKeyValueCtxBuilder::new().build(),
+//!     });
+//!
+//!     let mut linker = Linker::<Ctx>::new(&engine);
+//!     wasmtime_wasi::add_to_linker_async(&mut linker)?;
+//!     // add `wasi-runtime-config` world's interfaces to the linker
+//!     wasmtime_wasi_keyvalue::add_to_linker(&mut linker, |h: &mut Ctx| {
+//!         WasiKeyValue::new(&h.wasi_keyvalue_ctx, &mut h.table)
+//!     })?;
+//!
+//!     // ... use `linker` to instantiate within `store` ...
+//!
+//!     Ok(())
+//! }
+//!
+//! struct Ctx {
+//!     table: ResourceTable,
+//!     wasi_ctx: WasiCtx,
+//!     wasi_keyvalue_ctx: WasiKeyValueCtx,
+//! }
+//!
+//! impl WasiView for Ctx {
+//!     fn table(&mut self) -> &mut ResourceTable { &mut self.table }
+//!     fn ctx(&mut self) -> &mut WasiCtx { &mut self.wasi_ctx }
+//! }
+//! ```
+//!
+//! [wasi-keyvalue]: https://github.com/WebAssembly/wasi-keyvalue
+//! [wasi:cli]: https://docs.rs/wasmtime-wasi/latest
+//! [wasi:http]: https://docs.rs/wasmtime-wasi-http/latest
+
+#![deny(missing_docs)]
+
+mod provider;
+mod generated {
+    wasmtime::component::bindgen!({
+        path: "wit",
+        world: "wasi:keyvalue/imports",
+        trappable_imports: true,
+        async: true,
+        with: {
+            "wasi:keyvalue/store/bucket": crate::Bucket,
+        },
+        trappable_error_type: {
+            "wasi:keyvalue/store/error" => crate::Error,
+        },
+    });
+}
+
+use self::generated::wasi::keyvalue;
+use anyhow::Result;
+use async_trait::async_trait;
+use std::collections::HashMap;
+use std::fmt::Display;
+use url::Url;
+use wasmtime::component::{Resource, ResourceTable, ResourceTableError};
+
+#[doc(hidden)]
+pub enum Error {
+    NoSuchStore,
+    AccessDenied,
+    Other(String),
+}
+
+impl From<ResourceTableError> for Error {
+    fn from(err: ResourceTableError) -> Self {
+        Self::Other(err.to_string())
+    }
+}
+
+pub(crate) fn to_other_error(e: impl Display) -> Error {
+    Error::Other(e.to_string())
+}
+
+#[doc(hidden)]
+pub struct Bucket {
+    inner: Box<dyn Host + Send>,
+}
+
+#[async_trait]
+trait Host {
+    async fn get(&mut self, key: String) -> Result<Option<Vec<u8>>, Error>;
+
+    async fn set(&mut self, key: String, value: Vec<u8>) -> Result<(), Error>;
+
+    async fn delete(&mut self, key: String) -> Result<(), Error>;
+
+    async fn exists(&mut self, key: String) -> Result<bool, Error>;
+
+    async fn list_keys(
+        &mut self,
+        cursor: Option<u64>,
+    ) -> Result<keyvalue::store::KeyResponse, Error>;
+
+    async fn increment(&mut self, key: String, delta: u64) -> Result<u64, Error>;
+
+    async fn get_many(
+        &mut self,
+        keys: Vec<String>,
+    ) -> Result<Vec<Option<(String, Vec<u8>)>>, Error>;
+
+    async fn set_many(&mut self, key_values: Vec<(String, Vec<u8>)>) -> Result<(), Error>;
+
+    async fn delete_many(&mut self, keys: Vec<String>) -> Result<(), Error>;
+}
+
+/// Builder-style structure used to create a [`WasiKeyValueCtx`].
+#[derive(Default)]
+pub struct WasiKeyValueCtxBuilder {
+    in_memory_data: HashMap<String, Vec<u8>>,
+    #[cfg(feature = "redis")]
+    allowed_redis_hosts: Vec<String>,
+    #[cfg(feature = "redis")]
+    redis_connection_timeout: Option<std::time::Duration>,
+    #[cfg(feature = "redis")]
+    redis_response_timeout: Option<std::time::Duration>,
+}
+
+impl WasiKeyValueCtxBuilder {
+    /// Creates a builder for a new context with default parameters set.
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    /// Preset data for the In-Memory provider.
+    pub fn in_memory_data<I, K, V>(mut self, data: I) -> Self
+    where
+        I: IntoIterator<Item = (K, V)>,
+        K: Into<String>,
+        V: Into<Vec<u8>>,
+    {
+        self.in_memory_data = data
+            .into_iter()
+            .map(|(k, v)| (k.into(), v.into()))
+            .collect();
+        self
+    }
+
+    /// Appends a list of Redis hosts to the allow-listed set each component gets
+    /// access to. It can be in the format `<hostname>[:port]` or a unix domain
+    /// socket path.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use wasmtime_wasi_keyvalue::WasiKeyValueCtxBuilder;
+    ///
+    /// # fn main() {
+    /// let ctx = WasiKeyValueCtxBuilder::new()
+    ///     .allow_redis_hosts(&["localhost:1234", "/var/run/redis.sock"])
+    ///     .build();
+    /// # }
+    /// ```
+    #[cfg(feature = "redis")]
+    pub fn allow_redis_hosts(mut self, hosts: &[impl AsRef<str>]) -> Self {
+        self.allowed_redis_hosts
+            .extend(hosts.iter().map(|h| h.as_ref().to_owned()));
+        self
+    }
+
+    /// Sets the connection timeout parameter for the Redis provider.
+    #[cfg(feature = "redis")]
+    pub fn redis_connection_timeout(mut self, t: std::time::Duration) -> Self {
+        self.redis_connection_timeout = Some(t);
+        self
+    }
+
+    /// Sets the response timeout parameter for the Redis provider.
+    #[cfg(feature = "redis")]
+    pub fn redis_response_timeout(mut self, t: std::time::Duration) -> Self {
+        self.redis_response_timeout = Some(t);
+        self
+    }
+
+    /// Uses the configured context so far to construct the final [`WasiKeyValueCtx`].
+    pub fn build(self) -> WasiKeyValueCtx {
+        WasiKeyValueCtx {
+            in_memory_data: self.in_memory_data,
+            #[cfg(feature = "redis")]
+            allowed_redis_hosts: self.allowed_redis_hosts,
+            #[cfg(feature = "redis")]
+            redis_connection_timeout: self.redis_connection_timeout,
+            #[cfg(feature = "redis")]
+            redis_response_timeout: self.redis_response_timeout,
+        }
+    }
+}
+
+/// Capture the state necessary for use in the `wasi-keyvalue` API implementation.
+pub struct WasiKeyValueCtx {
+    in_memory_data: HashMap<String, Vec<u8>>,
+    #[cfg(feature = "redis")]
+    allowed_redis_hosts: Vec<String>,
+    #[cfg(feature = "redis")]
+    redis_connection_timeout: Option<std::time::Duration>,
+    #[cfg(feature = "redis")]
+    redis_response_timeout: Option<std::time::Duration>,
+}
+
+impl WasiKeyValueCtx {
+    /// Convenience function for calling [`WasiKeyValueCtxBuilder::new`].
+    pub fn builder() -> WasiKeyValueCtxBuilder {
+        WasiKeyValueCtxBuilder::new()
+    }
+
+    #[cfg(feature = "redis")]
+    fn allow_redis_host(&self, u: &Url) -> bool {
+        let host = match u.host() {
+            Some(h) => match u.port() {
+                Some(port) => format!("{}:{}", h, port),
+                None => h.to_string(),
+            },
+            // unix domain socket path
+            None => u.path().to_string(),
+        };
+        self.allowed_redis_hosts.contains(&host)
+    }
+}
+
+/// A wrapper capturing the needed internal `wasi-keyvalue` state.
+pub struct WasiKeyValue<'a> {
+    ctx: &'a WasiKeyValueCtx,
+    table: &'a mut ResourceTable,
+}
+
+impl<'a> WasiKeyValue<'a> {
+    /// Create a new view into the `wasi-keyvalue` state.
+    pub fn new(ctx: &'a WasiKeyValueCtx, table: &'a mut ResourceTable) -> Self {
+        Self { ctx, table }
+    }
+}
+
+#[async_trait]
+impl keyvalue::store::Host for WasiKeyValue<'_> {
+    async fn open(&mut self, identifier: String) -> Result<Resource<Bucket>, Error> {
+        if identifier == "" {
+            return Ok(self.table.push(Bucket {
+                inner: Box::new(provider::inmemory::InMemory::new(
+                    self.ctx.in_memory_data.clone(),
+                )),
+            })?);
+        }
+
+        let u = Url::parse(&identifier).map_err(to_other_error)?;
+        match u.scheme() {
+            "redis" | "redis+unix" => {
+                #[cfg(not(feature = "redis"))]
+                {
+                    return Err(Error::Other(
+                        "Cannot enable Redis support when the crate is not compiled with this feature."
+                            .to_string(),
+                    ));
+                }
+                #[cfg(feature = "redis")]
+                {
+                    if !self.ctx.allow_redis_host(&u) {
+                        return Err(Error::Other(format!(
+                            "the identifier {} is not in the allowed list",
+                            identifier
+                        )));
+                    }
+
+                    let host = provider::redis::open(
+                        identifier,
+                        self.ctx.redis_response_timeout,
+                        self.ctx.redis_connection_timeout,
+                    )
+                    .await?;
+                    Ok(self.table.push(Bucket {
+                        inner: Box::new(host),
+                    })?)
+                }
+            }
+            _ => Err(Error::NoSuchStore),
+        }
+    }
+
+    fn convert_error(&mut self, err: Error) -> Result<keyvalue::store::Error> {
+        match err {
+            Error::NoSuchStore => Ok(keyvalue::store::Error::NoSuchStore),
+            Error::AccessDenied => Ok(keyvalue::store::Error::AccessDenied),
+            Error::Other(e) => Ok(keyvalue::store::Error::Other(e)),
+        }
+    }
+}
+
+#[async_trait]
+impl keyvalue::store::HostBucket for WasiKeyValue<'_> {
+    async fn get(
+        &mut self,
+        bucket: Resource<Bucket>,
+        key: String,
+    ) -> Result<Option<Vec<u8>>, Error> {
+        let bucket = self.table.get_mut(&bucket)?;
+        bucket.inner.get(key).await
+    }
+
+    async fn set(
+        &mut self,
+        bucket: Resource<Bucket>,
+        key: String,
+        value: Vec<u8>,
+    ) -> Result<(), Error> {
+        let bucket = self.table.get_mut(&bucket)?;
+        bucket.inner.set(key, value).await
+    }
+
+    async fn delete(&mut self, bucket: Resource<Bucket>, key: String) -> Result<(), Error> {
+        let bucket = self.table.get_mut(&bucket)?;
+        bucket.inner.delete(key).await
+    }
+
+    async fn exists(&mut self, bucket: Resource<Bucket>, key: String) -> Result<bool, Error> {
+        let bucket = self.table.get_mut(&bucket)?;
+        bucket.inner.exists(key).await
+    }
+
+    async fn list_keys(
+        &mut self,
+        bucket: Resource<Bucket>,
+        cursor: Option<u64>,
+    ) -> Result<keyvalue::store::KeyResponse, Error> {
+        let bucket = self.table.get_mut(&bucket)?;
+        bucket.inner.list_keys(cursor).await
+    }
+
+    fn drop(&mut self, bucket: Resource<Bucket>) -> Result<()> {
+        self.table.delete(bucket)?;
+        Ok(())
+    }
+}
+
+#[async_trait]
+impl keyvalue::atomics::Host for WasiKeyValue<'_> {
+    async fn increment(
+        &mut self,
+        bucket: Resource<Bucket>,
+        key: String,
+        delta: u64,
+    ) -> Result<u64, Error> {
+        let bucket = self.table.get_mut(&bucket)?;
+        bucket.inner.increment(key, delta).await
+    }
+}
+
+#[async_trait]
+impl keyvalue::batch::Host for WasiKeyValue<'_> {
+    async fn get_many(
+        &mut self,
+        bucket: Resource<Bucket>,
+        keys: Vec<String>,
+    ) -> Result<Vec<Option<(String, Vec<u8>)>>, Error> {
+        let bucket = self.table.get_mut(&bucket)?;
+        bucket.inner.get_many(keys).await
+    }
+
+    async fn set_many(
+        &mut self,
+        bucket: Resource<Bucket>,
+        key_values: Vec<(String, Vec<u8>)>,
+    ) -> Result<(), Error> {
+        let bucket = self.table.get_mut(&bucket)?;
+        bucket.inner.set_many(key_values).await
+    }
+
+    async fn delete_many(
+        &mut self,
+        bucket: Resource<Bucket>,
+        keys: Vec<String>,
+    ) -> Result<(), Error> {
+        let bucket = self.table.get_mut(&bucket)?;
+        bucket.inner.delete_many(keys).await
+    }
+}
+
+/// Add all the `wasi-keyvalue` world's interfaces to a [`wasmtime::component::Linker`].
+pub fn add_to_linker<T: Send>(
+    l: &mut wasmtime::component::Linker<T>,
+    f: impl Fn(&mut T) -> WasiKeyValue<'_> + Send + Sync + Copy + 'static,
+) -> Result<()> {
+    keyvalue::store::add_to_linker_get_host(l, f)?;
+    keyvalue::atomics::add_to_linker_get_host(l, f)?;
+    keyvalue::batch::add_to_linker_get_host(l, f)?;
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    #[cfg(feature = "redis")]
+    fn test_allow_redis_host() {
+        let ctx = super::WasiKeyValueCtx::builder()
+            .allow_redis_hosts(&["127.0.0.1:1234", "localhost", "/var/run/redis.sock"])
+            .build();
+        assert!(ctx.allow_redis_host(&super::Url::parse("redis://127.0.0.1:1234/db").unwrap()));
+        assert!(ctx.allow_redis_host(&super::Url::parse("redis://localhost").unwrap()));
+        assert!(!ctx.allow_redis_host(&super::Url::parse("redis://192.168.0.1").unwrap()));
+        assert!(ctx.allow_redis_host(
+            &super::Url::parse("redis+unix:///var/run/redis.sock?db=db").unwrap()
+        ));
+        assert!(!ctx.allow_redis_host(
+            &super::Url::parse("redis+unix:///var/local/redis.sock?db=db").unwrap()
+        ));
+    }
+}
diff --git a/crates/wasi-keyvalue/src/provider/inmemory.rs b/crates/wasi-keyvalue/src/provider/inmemory.rs
new file mode 100644
index 000000000..f0ba02236
--- /dev/null
+++ b/crates/wasi-keyvalue/src/provider/inmemory.rs
@@ -0,0 +1,94 @@
+use crate::{generated::wasi::keyvalue::store::KeyResponse, to_other_error, Error, Host};
+use async_trait::async_trait;
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+
+#[derive(Default)]
+pub(crate) struct InMemory {
+    store: Arc<Mutex<HashMap<String, Vec<u8>>>>,
+}
+
+impl InMemory {
+    pub(crate) fn new(data: HashMap<String, Vec<u8>>) -> Self {
+        Self {
+            store: Arc::new(Mutex::new(data)),
+        }
+    }
+}
+
+#[async_trait]
+impl Host for InMemory {
+    async fn get(&mut self, key: String) -> Result<Option<Vec<u8>>, Error> {
+        let store = self.store.lock().unwrap();
+        Ok(store.get(&key).cloned())
+    }
+
+    async fn set(&mut self, key: String, value: Vec<u8>) -> Result<(), Error> {
+        let mut store = self.store.lock().unwrap();
+        store.insert(key, value);
+        Ok(())
+    }
+
+    async fn delete(&mut self, key: String) -> Result<(), Error> {
+        let mut store = self.store.lock().unwrap();
+        store.remove(&key);
+        Ok(())
+    }
+
+    async fn exists(&mut self, key: String) -> Result<bool, Error> {
+        let store = self.store.lock().unwrap();
+        Ok(store.contains_key(&key))
+    }
+
+    async fn list_keys(&mut self, cursor: Option<u64>) -> Result<KeyResponse, Error> {
+        let store = self.store.lock().unwrap();
+        let keys: Vec<String> = store.keys().cloned().collect();
+        let cursor = cursor.unwrap_or(0) as usize;
+        let keys_slice = &keys[cursor..];
+        Ok(KeyResponse {
+            keys: keys_slice.to_vec(),
+            cursor: None,
+        })
+    }
+
+    async fn increment(&mut self, key: String, delta: u64) -> Result<u64, Error> {
+        let mut store = self.store.lock().unwrap();
+        let value = store
+            .entry(key.clone())
+            .or_insert("0".to_string().into_bytes());
+        let current_value = String::from_utf8(value.clone())
+            .map_err(to_other_error)?
+            .parse::<u64>()
+            .map_err(to_other_error)?;
+        let new_value = current_value + delta;
+        *value = new_value.to_string().into_bytes();
+        Ok(new_value)
+    }
+
+    async fn get_many(
+        &mut self,
+        keys: Vec<String>,
+    ) -> Result<Vec<Option<(String, Vec<u8>)>>, Error> {
+        let store = self.store.lock().unwrap();
+        Ok(keys
+            .into_iter()
+            .map(|key| store.get(&key).map(|value| (key.clone(), value.clone())))
+            .collect())
+    }
+
+    async fn set_many(&mut self, key_values: Vec<(String, Vec<u8>)>) -> Result<(), Error> {
+        let mut store = self.store.lock().unwrap();
+        for (key, value) in key_values {
+            store.insert(key, value);
+        }
+        Ok(())
+    }
+
+    async fn delete_many(&mut self, keys: Vec<String>) -> Result<(), Error> {
+        let mut store = self.store.lock().unwrap();
+        for key in keys {
+            store.remove(&key);
+        }
+        Ok(())
+    }
+}
diff --git a/crates/wasi-keyvalue/src/provider/mod.rs b/crates/wasi-keyvalue/src/provider/mod.rs
new file mode 100644
index 000000000..1ea5bdb90
--- /dev/null
+++ b/crates/wasi-keyvalue/src/provider/mod.rs
@@ -0,0 +1,3 @@
+pub(crate) mod inmemory;
+#[cfg(feature = "redis")]
+pub(crate) mod redis;
diff --git a/crates/wasi-keyvalue/src/provider/redis.rs b/crates/wasi-keyvalue/src/provider/redis.rs
new file mode 100644
index 000000000..4020e8018
--- /dev/null
+++ b/crates/wasi-keyvalue/src/provider/redis.rs
@@ -0,0 +1,106 @@
+use crate::{generated::wasi::keyvalue::store::KeyResponse, Error, Host};
+use anyhow::Result;
+use async_trait::async_trait;
+use redis::{aio::MultiplexedConnection, AsyncCommands, RedisError};
+use std::time::Duration;
+
+struct Redis {
+    conn: MultiplexedConnection,
+}
+
+impl From<RedisError> for Error {
+    fn from(err: RedisError) -> Self {
+        Self::Other(err.to_string())
+    }
+}
+
+pub(crate) async fn open(
+    identifier: String,
+    response_timeout: Option<Duration>,
+    connection_timeout: Option<Duration>,
+) -> Result<impl Host, RedisError> {
+    let client = redis::Client::open(identifier)?;
+    let conn = client
+        .get_multiplexed_async_connection_with_timeouts(
+            response_timeout.unwrap_or(Duration::MAX),
+            connection_timeout.unwrap_or(Duration::MAX),
+        )
+        .await?;
+    Ok(Redis { conn })
+}
+
+#[async_trait]
+impl Host for Redis {
+    async fn get(&mut self, key: String) -> Result<Option<Vec<u8>>, Error> {
+        let v: Option<Vec<u8>> = self.conn.get(key).await?;
+        Ok(v)
+    }
+
+    async fn set(&mut self, key: String, value: Vec<u8>) -> Result<(), Error> {
+        let _: () = self.conn.set(key, value).await?;
+        Ok(())
+    }
+
+    async fn delete(&mut self, key: String) -> Result<(), Error> {
+        let _: () = self.conn.del(key).await?;
+        Ok(())
+    }
+
+    async fn exists(&mut self, key: String) -> Result<bool, Error> {
+        let exists: bool = self.conn.exists(key).await?;
+        Ok(exists)
+    }
+
+    async fn list_keys(&mut self, cursor: Option<u64>) -> Result<KeyResponse, Error> {
+        let cursor = cursor.unwrap_or(0);
+        let (new_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
+            .arg(cursor)
+            .query_async(&mut self.conn)
+            .await?;
+
+        Ok(KeyResponse {
+            keys,
+            cursor: if new_cursor == 0 {
+                None
+            } else {
+                Some(new_cursor)
+            },
+        })
+    }
+
+    async fn increment(&mut self, key: String, delta: u64) -> Result<u64, Error> {
+        let v: u64 = self.conn.incr(key, delta).await?;
+        Ok(v)
+    }
+
+    async fn get_many(
+        &mut self,
+        keys: Vec<String>,
+    ) -> Result<Vec<Option<(String, Vec<u8>)>>, Error> {
+        let values: Vec<Option<Vec<u8>>> = self.conn.get(keys.clone()).await?;
+
+        Ok(keys
+            .into_iter()
+            .zip(values.into_iter())
+            .map(|(key, value)| value.map(|v| (key, v)))
+            .collect())
+    }
+
+    async fn set_many(&mut self, key_values: Vec<(String, Vec<u8>)>) -> Result<(), Error> {
+        let mut pipe = redis::pipe();
+        for (key, value) in key_values {
+            pipe.set(key, value).ignore();
+        }
+        pipe.query_async(&mut self.conn).await?;
+        Ok(())
+    }
+
+    async fn delete_many(&mut self, keys: Vec<String>) -> Result<(), Error> {
+        let mut pipe = redis::pipe();
+        for key in keys {
+            pipe.del(key).ignore();
+        }
+        pipe.query_async(&mut self.conn).await?;
+        Ok(())
+    }
+}
diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/atomic.wit b/crates/wasi-keyvalue/wit/deps/keyvalue/atomic.wit
new file mode 100644
index 000000000..059efc488
--- /dev/null
+++ b/crates/wasi-keyvalue/wit/deps/keyvalue/atomic.wit
@@ -0,0 +1,22 @@
+/// A keyvalue interface that provides atomic operations.
+/// 
+/// Atomic operations are single, indivisible operations. When a fault causes an atomic operation to
+/// fail, it will appear to the invoker of the atomic operation that the action either completed
+/// successfully or did nothing at all.
+/// 
+/// Please note that this interface is bare functions that take a reference to a bucket. This is to
+/// get around the current lack of a way to "extend" a resource with additional methods inside of
+/// wit. Future version of the interface will instead extend these methods on the base `bucket`
+/// resource.
+interface atomics {
+  	use store.{bucket, error};
+
+  	/// Atomically increment the value associated with the key in the store by the given delta. It
+	/// returns the new value.
+	///
+	/// If the key does not exist in the store, it creates a new key-value pair with the value set
+	/// to the given delta. 
+	///
+	/// If any other error occurs, it returns an `Err(error)`.
+	increment: func(bucket: borrow<bucket>, key: string, delta: u64) -> result<u64, error>;
+}
\ No newline at end of file
diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/batch.wit b/crates/wasi-keyvalue/wit/deps/keyvalue/batch.wit
new file mode 100644
index 000000000..70c05feb9
--- /dev/null
+++ b/crates/wasi-keyvalue/wit/deps/keyvalue/batch.wit
@@ -0,0 +1,63 @@
+/// A keyvalue interface that provides batch operations.
+/// 
+/// A batch operation is an operation that operates on multiple keys at once.
+/// 
+/// Batch operations are useful for reducing network round-trip time. For example, if you want to
+/// get the values associated with 100 keys, you can either do 100 get operations or you can do 1
+/// batch get operation. The batch operation is faster because it only needs to make 1 network call
+/// instead of 100.
+/// 
+/// A batch operation does not guarantee atomicity, meaning that if the batch operation fails, some
+/// of the keys may have been modified and some may not. 
+/// 
+/// This interface does has the same consistency guarantees as the `store` interface, meaning that
+/// you should be able to "read your writes."
+/// 
+/// Please note that this interface is bare functions that take a reference to a bucket. This is to
+/// get around the current lack of a way to "extend" a resource with additional methods inside of
+/// wit. Future version of the interface will instead extend these methods on the base `bucket`
+/// resource.
+interface batch {
+    use store.{bucket, error};
+
+    /// Get the key-value pairs associated with the keys in the store. It returns a list of
+    /// key-value pairs.
+    ///
+    /// If any of the keys do not exist in the store, it returns a `none` value for that pair in the
+    /// list.
+    /// 
+    /// MAY show an out-of-date value if there are concurrent writes to the store.
+    /// 
+    /// If any other error occurs, it returns an `Err(error)`.
+    get-many: func(bucket: borrow<bucket>, keys: list<string>) -> result<list<option<tuple<string, list<u8>>>>, error>;
+
+    /// Set the values associated with the keys in the store. If the key already exists in the
+    /// store, it overwrites the value. 
+    /// 
+    /// Note that the key-value pairs are not guaranteed to be set in the order they are provided. 
+    ///
+    /// If any of the keys do not exist in the store, it creates a new key-value pair.
+    /// 
+    /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not
+    /// rollback the key-value pairs that were already set. Thus, this batch operation does not
+    /// guarantee atomicity, implying that some key-value pairs could be set while others might
+    /// fail. 
+    /// 
+    /// Other concurrent operations may also be able to see the partial results.
+    set-many: func(bucket: borrow<bucket>, key-values: list<tuple<string, list<u8>>>) -> result<_, error>;
+
+    /// Delete the key-value pairs associated with the keys in the store.
+    /// 
+    /// Note that the key-value pairs are not guaranteed to be deleted in the order they are
+    /// provided.
+    /// 
+    /// If any of the keys do not exist in the store, it skips the key.
+    /// 
+    /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not
+    /// rollback the key-value pairs that were already deleted. Thus, this batch operation does not
+    /// guarantee atomicity, implying that some key-value pairs could be deleted while others might
+    /// fail.
+    /// 
+    /// Other concurrent operations may also be able to see the partial results.
+    delete-many: func(bucket: borrow<bucket>, keys: list<string>) -> result<_, error>;
+}
diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/store.wit b/crates/wasi-keyvalue/wit/deps/keyvalue/store.wit
new file mode 100644
index 000000000..3354ea2f3
--- /dev/null
+++ b/crates/wasi-keyvalue/wit/deps/keyvalue/store.wit
@@ -0,0 +1,122 @@
+/// A keyvalue interface that provides eventually consistent key-value operations.
+/// 
+/// Each of these operations acts on a single key-value pair.
+/// 
+/// The value in the key-value pair is defined as a `u8` byte array and the intention is that it is
+/// the common denominator for all data types defined by different key-value stores to handle data,
+/// ensuring compatibility between different key-value stores. Note: the clients will be expecting
+/// serialization/deserialization overhead to be handled by the key-value store. The value could be
+/// a serialized object from JSON, HTML or vendor-specific data types like AWS S3 objects.
+/// 
+/// Data consistency in a key value store refers to the guarantee that once a write operation
+/// completes, all subsequent read operations will return the value that was written.
+/// 
+/// Any implementation of this interface must have enough consistency to guarantee "reading your
+/// writes." In particular, this means that the client should never get a value that is older than
+/// the one it wrote, but it MAY get a newer value if one was written around the same time. These
+/// guarantees only apply to the same client (which will likely be provided by the host or an
+/// external capability of some kind). In this context a "client" is referring to the caller or
+/// guest that is consuming this interface. Once a write request is committed by a specific client,
+/// all subsequent read requests by the same client will reflect that write or any subsequent
+/// writes. Another client running in a different context may or may not immediately see the result
+/// due to the replication lag. As an example of all of this, if a value at a given key is A, and
+/// the client writes B, then immediately reads, it should get B. If something else writes C in
+/// quick succession, then the client may get C. However, a client running in a separate context may
+/// still see A or B
+interface store {
+    /// The set of errors which may be raised by functions in this package
+    variant error {
+        /// The host does not recognize the store identifier requested.
+        no-such-store,
+
+        /// The requesting component does not have access to the specified store
+        /// (which may or may not exist).
+        access-denied,
+
+        /// Some implementation-specific error has occurred (e.g. I/O)
+        other(string)
+    }
+
+    /// A response to a `list-keys` operation.
+    record key-response {
+        /// The list of keys returned by the query.
+        keys: list<string>,
+        /// The continuation token to use to fetch the next page of keys. If this is `null`, then
+        /// there are no more keys to fetch.
+        cursor: option<u64>
+    }
+
+    /// Get the bucket with the specified identifier.
+    ///
+    /// `identifier` must refer to a bucket provided by the host.
+    ///
+    /// `error::no-such-store` will be raised if the `identifier` is not recognized.
+    open: func(identifier: string) -> result<bucket, error>;
+
+    /// A bucket is a collection of key-value pairs. Each key-value pair is stored as a entry in the
+    /// bucket, and the bucket itself acts as a collection of all these entries.
+    ///
+    /// It is worth noting that the exact terminology for bucket in key-value stores can very
+    /// depending on the specific implementation. For example:
+    ///
+    /// 1. Amazon DynamoDB calls a collection of key-value pairs a table
+    /// 2. Redis has hashes, sets, and sorted sets as different types of collections
+    /// 3. Cassandra calls a collection of key-value pairs a column family
+    /// 4. MongoDB calls a collection of key-value pairs a collection
+    /// 5. Riak calls a collection of key-value pairs a bucket
+    /// 6. Memcached calls a collection of key-value pairs a slab
+    /// 7. Azure Cosmos DB calls a collection of key-value pairs a container
+    ///
+    /// In this interface, we use the term `bucket` to refer to a collection of key-value pairs
+    resource bucket {
+        /// Get the value associated with the specified `key`
+        ///
+        /// The value is returned as an option. If the key-value pair exists in the
+        /// store, it returns `Ok(value)`. If the key does not exist in the
+        /// store, it returns `Ok(none)`. 
+        ///
+        /// If any other error occurs, it returns an `Err(error)`.
+        get: func(key: string) -> result<option<list<u8>>, error>;
+
+        /// Set the value associated with the key in the store. If the key already
+        /// exists in the store, it overwrites the value.
+        ///
+        /// If the key does not exist in the store, it creates a new key-value pair.
+        /// 
+        /// If any other error occurs, it returns an `Err(error)`.
+        set: func(key: string, value: list<u8>) -> result<_, error>;
+
+        /// Delete the key-value pair associated with the key in the store.
+        /// 
+        /// If the key does not exist in the store, it does nothing.
+        ///
+        /// If any other error occurs, it returns an `Err(error)`.
+        delete: func(key: string) -> result<_, error>;
+
+        /// Check if the key exists in the store.
+        /// 
+        /// If the key exists in the store, it returns `Ok(true)`. If the key does
+        /// not exist in the store, it returns `Ok(false)`.
+        /// 
+        /// If any other error occurs, it returns an `Err(error)`.
+        exists: func(key: string) -> result<bool, error>;
+
+        /// Get all the keys in the store with an optional cursor (for use in pagination). It
+        /// returns a list of keys. Please note that for most KeyValue implementations, this is a
+        /// can be a very expensive operation and so it should be used judiciously. Implementations
+        /// can return any number of keys in a single response, but they should never attempt to
+        /// send more data than is reasonable (i.e. on a small edge device, this may only be a few
+        /// KB, while on a large machine this could be several MB). Any response should also return
+        /// a cursor that can be used to fetch the next page of keys. See the `key-response` record
+        /// for more information.
+        /// 
+        /// Note that the keys are not guaranteed to be returned in any particular order.
+        /// 
+        /// If the store is empty, it returns an empty list.
+        /// 
+        /// MAY show an out-of-date list of keys if there are concurrent writes to the store.
+        /// 
+        /// If any error occurs, it returns an `Err(error)`.
+        list-keys: func(cursor: option<u64>) -> result<key-response, error>;
+    }
+}
diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/watch.wit b/crates/wasi-keyvalue/wit/deps/keyvalue/watch.wit
new file mode 100644
index 000000000..ff13f7523
--- /dev/null
+++ b/crates/wasi-keyvalue/wit/deps/keyvalue/watch.wit
@@ -0,0 +1,16 @@
+/// A keyvalue interface that provides watch operations.
+/// 
+/// This interface is used to provide event-driven mechanisms to handle
+/// keyvalue changes.
+interface watcher {
+	/// A keyvalue interface that provides handle-watch operations.
+	use store.{bucket};
+
+	/// Handle the `set` event for the given bucket and key. It includes a reference to the `bucket`
+	/// that can be used to interact with the store.
+	on-set: func(bucket: bucket, key: string, value: list<u8>);
+
+	/// Handle the `delete` event for the given bucket and key. It includes a reference to the
+	/// `bucket` that can be used to interact with the store.
+	on-delete: func(bucket: bucket, key: string);
+}
\ No newline at end of file
diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/world.wit b/crates/wasi-keyvalue/wit/deps/keyvalue/world.wit
new file mode 100644
index 000000000..066148c1f
--- /dev/null
+++ b/crates/wasi-keyvalue/wit/deps/keyvalue/world.wit
@@ -0,0 +1,26 @@
+package wasi:keyvalue@0.2.0-draft;
+
+/// The `wasi:keyvalue/imports` world provides common APIs for interacting with key-value stores.
+/// Components targeting this world will be able to do:
+/// 
+/// 1. CRUD (create, read, update, delete) operations on key-value stores.
+/// 2. Atomic `increment` and CAS (compare-and-swap) operations.
+/// 3. Batch operations that can reduce the number of round trips to the network.
+world imports {
+	/// The `store` capability allows the component to perform eventually consistent operations on
+	/// the key-value store.
+	import store;
+
+	/// The `atomic` capability allows the component to perform atomic / `increment` and CAS
+	/// (compare-and-swap) operations.
+	import atomics;
+
+	/// The `batch` capability allows the component to perform eventually consistent batch
+	/// operations that can reduce the number of round trips to the network.
+	import batch;
+}
+
+world watch-service {
+	include imports;
+	export watcher;
+}
\ No newline at end of file
diff --git a/crates/wasi-keyvalue/wit/world.wit b/crates/wasi-keyvalue/wit/world.wit
new file mode 100644
index 000000000..1d4c6ab44
--- /dev/null
+++ b/crates/wasi-keyvalue/wit/world.wit
@@ -0,0 +1,6 @@
+// We actually don't use this; it's just to let bindgen! find the corresponding world in wit/deps.
+package wasmtime:wasi;
+
+world bindings {
+  include wasi:keyvalue/imports@0.2.0-draft;
+}
diff --git a/crates/wasi/src/stdio.rs b/crates/wasi/src/stdio.rs
index a8865927d..72658ec27 100644
--- a/crates/wasi/src/stdio.rs
+++ b/crates/wasi/src/stdio.rs
@@ -50,7 +50,7 @@ impl StdinStream for pipe::MemoryInputPipe {
 
 impl StdinStream for pipe::ClosedInputStream {
     fn stream(&self) -> Box<dyn HostInputStream> {
-        Box::new(self.clone())
+        Box::new(*self)
     }
 
     fn isatty(&self) -> bool {
@@ -160,7 +160,7 @@ impl StdoutStream for pipe::MemoryOutputPipe {
 
 impl StdoutStream for pipe::SinkOutputStream {
     fn stream(&self) -> Box<dyn HostOutputStream> {
-        Box::new(self.clone())
+        Box::new(*self)
     }
 
     fn isatty(&self) -> bool {
@@ -170,7 +170,7 @@ impl StdoutStream for pipe::SinkOutputStream {
 
 impl StdoutStream for pipe::ClosedOutputStream {
     fn stream(&self) -> Box<dyn HostOutputStream> {
-        Box::new(self.clone())
+        Box::new(*self)
     }
 
     fn isatty(&self) -> bool {
diff --git a/crates/wasmtime/src/compile.rs b/crates/wasmtime/src/compile.rs
index 096263522..a5fcfc15a 100644
--- a/crates/wasmtime/src/compile.rs
+++ b/crates/wasmtime/src/compile.rs
@@ -70,7 +70,7 @@ pub(crate) fn build_artifacts<T: FinishedObject>(
     // validated. Afterwards `types` will have all the type information for
     // this module.
     let mut parser = wasmparser::Parser::new(0);
-    let mut validator = wasmparser::Validator::new_with_features(engine.config().features.clone());
+    let mut validator = wasmparser::Validator::new_with_features(engine.config().features);
     parser.set_features(*validator.features());
     let mut types = ModuleTypesBuilder::new(&validator);
     let mut translation = ModuleEnvironment::new(tunables, &mut validator, &mut types)
@@ -136,7 +136,7 @@ pub(crate) fn build_component_artifacts<T: FinishedObject>(
     let compiler = engine.compiler();
 
     let scope = ScopeVec::new();
-    let mut validator = wasmparser::Validator::new_with_features(engine.config().features.clone());
+    let mut validator = wasmparser::Validator::new_with_features(engine.config().features);
     let mut types = ComponentTypesBuilder::new(&validator);
     let (component, mut module_translations) =
         Translator::new(tunables, &mut validator, &mut types, &scope)
diff --git a/crates/wasmtime/src/runtime/component/linker.rs b/crates/wasmtime/src/runtime/component/linker.rs
index fd98445b7..3e2e548f5 100644
--- a/crates/wasmtime/src/runtime/component/linker.rs
+++ b/crates/wasmtime/src/runtime/component/linker.rs
@@ -234,7 +234,7 @@ impl<T> Linker<T> {
                 Definition::Module(m) => RuntimeImport::Module(m.clone()),
                 Definition::Func(f) => RuntimeImport::Func(f.clone()),
                 Definition::Resource(t, dtor) => RuntimeImport::Resource {
-                    ty: t.clone(),
+                    ty: *t,
                     _dtor: dtor.clone(),
                     dtor_funcref: component.resource_drop_func_ref(dtor),
                 },
diff --git a/crates/wasmtime/src/runtime/gc/enabled/structref.rs b/crates/wasmtime/src/runtime/gc/enabled/structref.rs
index d0ae4aeb7..48849e405 100644
--- a/crates/wasmtime/src/runtime/gc/enabled/structref.rs
+++ b/crates/wasmtime/src/runtime/gc/enabled/structref.rs
@@ -274,7 +274,7 @@ impl StructRef {
                     allocator.layout(),
                     ty.element_type(),
                     index,
-                    val.clone(),
+                    *val,
                 )?;
             }
             Ok(())
diff --git a/crates/wasmtime/src/runtime/linker.rs b/crates/wasmtime/src/runtime/linker.rs
index f49a4d6b6..4f021fe90 100644
--- a/crates/wasmtime/src/runtime/linker.rs
+++ b/crates/wasmtime/src/runtime/linker.rs
@@ -1310,7 +1310,7 @@ impl<T> Linker<T> {
     ) -> Result<Func> {
         if let Some(external) = self.get(&mut store, module, "") {
             if let Extern::Func(func) = external {
-                return Ok(func.clone());
+                return Ok(func);
             }
             bail!("default export in '{}' is not a function", module);
         }
@@ -1318,7 +1318,7 @@ impl<T> Linker<T> {
         // For compatibility, also recognize "_start".
         if let Some(external) = self.get(&mut store, module, "_start") {
             if let Extern::Func(func) = external {
-                return Ok(func.clone());
+                return Ok(func);
             }
             bail!("`_start` in '{}' is not a function", module);
         }
diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs
index a794e4159..a8fc20b62 100644
--- a/crates/wasmtime/src/runtime/memory.rs
+++ b/crates/wasmtime/src/runtime/memory.rs
@@ -808,7 +808,7 @@ impl SharedMemory {
         debug_assert!(ty.maximum().is_some());
 
         let tunables = engine.tunables();
-        let plan = MemoryPlan::for_memory(ty.wasmtime_memory().clone(), tunables);
+        let plan = MemoryPlan::for_memory(*ty.wasmtime_memory(), tunables);
         let page_size_log2 = plan.memory.page_size_log2;
         let memory = crate::runtime::vm::SharedMemory::new(plan)?;
 
diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs
index 7eab04580..e3e9f0f72 100644
--- a/crates/wasmtime/src/runtime/store.rs
+++ b/crates/wasmtime/src/runtime/store.rs
@@ -2042,7 +2042,7 @@ at https://bytecodealliance.org/security.
     /// Retrieve the store's protection key.
     #[inline]
     pub(crate) fn get_pkey(&self) -> Option<ProtectionKey> {
-        self.pkey.clone()
+        self.pkey
     }
 
     #[inline]
diff --git a/crates/wasmtime/src/runtime/trampoline/memory.rs b/crates/wasmtime/src/runtime/trampoline/memory.rs
index 97a1cd0ff..ca3571932 100644
--- a/crates/wasmtime/src/runtime/trampoline/memory.rs
+++ b/crates/wasmtime/src/runtime/trampoline/memory.rs
@@ -38,7 +38,7 @@ pub fn create_memory(
     // constructing a memory with an allocator: instead the memories are either
     // preallocated (i.e., shared memory) or allocated manually below.
     let plan = wasmtime_environ::MemoryPlan::for_memory(
-        memory_ty.wasmtime_memory().clone(),
+        *memory_ty.wasmtime_memory(),
         store.engine().tunables(),
     );
     let memory_id = module.memory_plans.push(plan.clone());
diff --git a/crates/wasmtime/src/runtime/trampoline/table.rs b/crates/wasmtime/src/runtime/trampoline/table.rs
index ce7f0bc9d..d96d6a295 100644
--- a/crates/wasmtime/src/runtime/trampoline/table.rs
+++ b/crates/wasmtime/src/runtime/trampoline/table.rs
@@ -7,7 +7,7 @@ use wasmtime_environ::{EntityIndex, Module, TypeTrace};
 pub fn create_table(store: &mut StoreOpaque, table: &TableType) -> Result<InstanceId> {
     let mut module = Module::new();
 
-    let wasmtime_table = table.wasmtime_table().clone();
+    let wasmtime_table = *table.wasmtime_table();
     let tunables = store.engine().tunables();
 
     debug_assert!(
diff --git a/crates/wasmtime/src/runtime/types.rs b/crates/wasmtime/src/runtime/types.rs
index d4f43c9e2..321cb7199 100644
--- a/crates/wasmtime/src/runtime/types.rs
+++ b/crates/wasmtime/src/runtime/types.rs
@@ -2431,7 +2431,7 @@ impl TableType {
         let element = RefType::from_wasm_type(engine, &table.wasm_ty);
         TableType {
             element,
-            ty: table.clone(),
+            ty: *table,
         }
     }
 
@@ -2734,7 +2734,7 @@ impl MemoryType {
     }
 
     pub(crate) fn from_wasmtime_memory(memory: &Memory) -> MemoryType {
-        MemoryType { ty: memory.clone() }
+        MemoryType { ty: *memory }
     }
 
     pub(crate) fn wasmtime_memory(&self) -> &Memory {
diff --git a/crates/wasmtime/src/runtime/values.rs b/crates/wasmtime/src/runtime/values.rs
index 1a3a2c7c8..44244d268 100644
--- a/crates/wasmtime/src/runtime/values.rs
+++ b/crates/wasmtime/src/runtime/values.rs
@@ -14,7 +14,7 @@ pub use crate::runtime::vm::ValRaw;
 ///
 /// Note that we inline the `enum Ref { ... }` variants into `enum Val { ... }`
 /// here as a size optimization.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Copy)]
 pub enum Val {
     // NB: the ordering here is intended to match the ordering in
     // `ValType` to improve codegen when learning the type of a value.
@@ -166,9 +166,7 @@ impl Val {
             | (Val::F64(_), ValType::F64)
             | (Val::V128(_), ValType::V128) => true,
 
-            (Val::FuncRef(f), ValType::Ref(ref_ty)) => {
-                Ref::from(f.clone())._matches_ty(store, ref_ty)?
-            }
+            (Val::FuncRef(f), ValType::Ref(ref_ty)) => Ref::from(*f)._matches_ty(store, ref_ty)?,
             (Val::ExternRef(e), ValType::Ref(ref_ty)) => {
                 Ref::from(*e)._matches_ty(store, ref_ty)?
             }
diff --git a/crates/wasmtime/src/runtime/vm/instance/allocator.rs b/crates/wasmtime/src/runtime/vm/instance/allocator.rs
index ee39886cb..61cb6fe4c 100644
--- a/crates/wasmtime/src/runtime/vm/instance/allocator.rs
+++ b/crates/wasmtime/src/runtime/vm/instance/allocator.rs
@@ -94,7 +94,7 @@ impl StorePtr {
 
     /// The raw contents of this struct
     pub fn as_raw(&self) -> Option<*mut dyn Store> {
-        self.0.clone()
+        self.0
     }
 
     /// Use the StorePtr as a mut ref to the Store.
diff --git a/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/memory_pool.rs b/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/memory_pool.rs
index 0abcfa79c..2e5dd793b 100644
--- a/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/memory_pool.rs
+++ b/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/memory_pool.rs
@@ -252,7 +252,7 @@ impl MemoryPool {
             self.stripes.len() < 2 || self.stripes[index].pkey.is_some(),
             "if we are using stripes, we cannot have an empty protection key"
         );
-        self.stripes[index].pkey.clone()
+        self.stripes[index].pkey
     }
 
     /// Validate whether this memory pool supports the given module.
diff --git a/crates/wiggle/generate/src/codegen_settings.rs b/crates/wiggle/generate/src/codegen_settings.rs
index b6fdf62f2..4b0e4d6bc 100644
--- a/crates/wiggle/generate/src/codegen_settings.rs
+++ b/crates/wiggle/generate/src/codegen_settings.rs
@@ -62,7 +62,7 @@ impl ErrorTransform {
                 },
                 ErrorConfField::User(field) => if let Some(abi_type) = doc.typename(&Id::new(ident.to_string())) {
                     if let Some(ident) = field.rich_error.get_ident() {
-                        if let Some(prior_def) = richtype_identifiers.insert(ident.clone(), field.err_loc.clone())
+                        if let Some(prior_def) = richtype_identifiers.insert(ident.clone(), field.err_loc)
                          {
                             return Err(anyhow!(
                                     "duplicate rich type identifier of {:?} not allowed. prior definition at {:?}",
diff --git a/scripts/publish.rs b/scripts/publish.rs
index 20322dc63..b3fcf2154 100644
--- a/scripts/publish.rs
+++ b/scripts/publish.rs
@@ -69,6 +69,7 @@ const CRATES_TO_PUBLISH: &[&str] = &[
     "wasmtime-wasi-http",
     "wasmtime-wasi-nn",
     "wasmtime-wasi-runtime-config",
+    "wasmtime-wasi-keyvalue",
     "wasmtime-wasi-threads",
     "wasmtime-wast",
     "wasmtime-c-api-macros",
@@ -89,6 +90,7 @@ const PUBLIC_CRATES: &[&str] = &[
     "wasmtime-wasi",
     "wasmtime-wasi-nn",
     "wasmtime-wasi-runtime-config",
+    "wasmtime-wasi-keyvalue",
     "wasmtime-wasi-threads",
     "wasmtime-cli",
     // all cranelift crates are considered "public" in that they can't
diff --git a/src/commands/explore.rs b/src/commands/explore.rs
index 84bc73c6e..d390ec067 100644
--- a/src/commands/explore.rs
+++ b/src/commands/explore.rs
@@ -55,6 +55,7 @@ impl ExploreCommand {
         let clif_dir = if let Some(Strategy::Cranelift) | None = self.common.codegen.compiler {
             let clif_dir = tempdir()?;
             config.emit_clif(clif_dir.path());
+            config.disable_cache(); // cache does not emit clif
             Some(clif_dir)
         } else {
             None
diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml
index aa6c21129..3f8c7d6d4 100644
--- a/supply-chain/audits.toml
+++ b/supply-chain/audits.toml
@@ -2424,6 +2424,11 @@ criteria = "safe-to-deploy"
 version = "1.0.17"
 notes = "plenty of unsafe pointer and vec tricks, but in well-structured and commented code that appears to be correct"
 
+[[audits.sha1_smol]]
+who = "Alex Crichton <alex@alexcrichton.com>"
+criteria = "safe-to-deploy"
+version = "1.0.1"
+
 [[audits.sha2]]
 who = "Benjamin Bouvier <public@benj.me>"
 criteria = "safe-to-deploy"
@@ -2476,6 +2481,11 @@ criteria = "safe-to-deploy"
 delta = "0.4.7 -> 0.4.9"
 notes = "Minor OS compat updates but otherwise nothing major here."
 
+[[audits.socket2]]
+who = "Alex Crichton <alex@alexcrichton.com>"
+criteria = "safe-to-deploy"
+delta = "0.4.9 -> 0.4.4"
+
 [[audits.spin]]
 who = "Alex Crichton <alex@alexcrichton.com>"
 criteria = "safe-to-run"
diff --git a/supply-chain/config.toml b/supply-chain/config.toml
index 290afc7a9..20fb7d75e 100644
--- a/supply-chain/config.toml
+++ b/supply-chain/config.toml
@@ -218,6 +218,10 @@ criteria = "safe-to-deploy"
 version = "0.2.7"
 criteria = "safe-to-run"
 
+[[exemptions.combine]]
+version = "4.6.7"
+criteria = "safe-to-deploy"
+
 [[exemptions.console]]
 version = "0.15.0"
 criteria = "safe-to-deploy"
@@ -486,6 +490,10 @@ criteria = "safe-to-deploy"
 version = "0.3.0"
 criteria = "safe-to-deploy"
 
+[[exemptions.redis]]
+version = "0.25.4"
+criteria = "safe-to-deploy"
+
 [[exemptions.redox_syscall]]
 version = "0.2.13"
 criteria = "safe-to-deploy"
diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock
index ff04c4955..2d7845bc0 100644
--- a/supply-chain/imports.lock
+++ b/supply-chain/imports.lock
@@ -4008,6 +4008,13 @@ version = "0.2.9"
 notes = "Reviewed on https://fxrev.dev/824504"
 aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT"
 
+[[audits.google.audits.socket2]]
+who = "David Koloski <dkoloski@google.com>"
+criteria = "safe-to-deploy"
+delta = "0.4.4 -> 0.5.5"
+notes = "Reviewed at https://fxrev.dev/946307"
+aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT"
+
 [[audits.google.audits.threadpool]]
 who = "Dennis Kempin <denniskempin@google.com>"
 criteria = "safe-to-run"
@@ -4545,6 +4552,12 @@ criteria = "safe-to-deploy"
 delta = "0.4.6 -> 0.4.7"
 aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
 
+[[audits.mozilla.audits.socket2]]
+who = "Kershaw Chang <kershaw@mozilla.com>"
+criteria = "safe-to-deploy"
+delta = "0.5.5 -> 0.5.7"
+aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
+
 [[audits.mozilla.audits.subtle]]
 who = "Simon Friedberger <simon@mozilla.com>"
 criteria = "safe-to-deploy"


Test patch:
diff --git a/ci/run-tests.sh b/ci/run-tests.sh
index 274d35232..1822a3fd0 100755
--- a/ci/run-tests.sh
+++ b/ci/run-tests.sh
@@ -12,6 +12,9 @@
 #
 # - wasm-spec-interpreter: brings in OCaml which is a pain to configure for all
 #   targets, tested as part of the wastime-fuzzing CI job.
+#
+# - wasmtime-wasi-keyvalue: additional database service dependencies, needs its
+#   own CI job.
 
 cargo test \
       --workspace \
@@ -20,4 +23,5 @@ cargo test \
       --exclude wasmtime-wasi-nn \
       --exclude wasmtime-fuzzing \
       --exclude wasm-spec-interpreter \
+      --exclude wasmtime-wasi-keyvalue \
       $@
diff --git a/crates/test-programs/artifacts/build.rs b/crates/test-programs/artifacts/build.rs
index 69fbe56b7..018b5cce9 100644
--- a/crates/test-programs/artifacts/build.rs
+++ b/crates/test-programs/artifacts/build.rs
@@ -77,6 +77,7 @@ fn build_and_generate_tests() {
             s if s.starts_with("piped_") => "piped",
             s if s.starts_with("dwarf_") => "dwarf",
             s if s.starts_with("runtime_config_") => "runtime_config",
+            s if s.starts_with("keyvalue_") => "keyvalue",
             // If you're reading this because you hit this panic, either add it
             // to a test suite above or add a new "suite". The purpose of the
             // categorization above is to have a static assertion that tests
diff --git a/crates/test-programs/src/bin/keyvalue_main.rs b/crates/test-programs/src/bin/keyvalue_main.rs
new file mode 100644
index 000000000..4fccb2c81
--- /dev/null
+++ b/crates/test-programs/src/bin/keyvalue_main.rs
@@ -0,0 +1,51 @@
+use test_programs::keyvalue::wasi::keyvalue::{atomics, batch, store};
+
+fn main() {
+    let identifier = std::env::var_os("IDENTIFIER")
+        .unwrap()
+        .into_string()
+        .unwrap();
+    let bucket = store::open(&identifier).unwrap();
+
+    if identifier != "" {
+        // for In-Memory provider, we have preset this data
+        assert_eq!(atomics::increment(&bucket, "atomics_key", 5).unwrap(), 5);
+    }
+    assert_eq!(atomics::increment(&bucket, "atomics_key", 1).unwrap(), 6);
+
+    let resp = bucket.list_keys(None).unwrap();
+    assert_eq!(resp.keys, vec!["atomics_key".to_string()]);
+
+    bucket.set("hello", "world".as_bytes()).unwrap();
+
+    let v = bucket.get("hello").unwrap();
+    assert_eq!(String::from_utf8(v.unwrap()).unwrap(), "world");
+
+    assert_eq!(bucket.exists("hello").unwrap(), true);
+    bucket.delete("hello").unwrap();
+    assert_eq!(bucket.exists("hello").unwrap(), false);
+
+    batch::set_many(
+        &bucket,
+        &[
+            ("a1".to_string(), "v1".as_bytes().to_vec()),
+            ("b1".to_string(), "v1".as_bytes().to_vec()),
+            ("c1".to_string(), "v1".as_bytes().to_vec()),
+        ],
+    )
+    .unwrap();
+    batch::delete_many(&bucket, &["a1".to_string(), "c1".to_string()]).unwrap();
+    let values = batch::get_many(
+        &bucket,
+        &["a1".to_string(), "b1".to_string(), "c1".to_string()],
+    )
+    .unwrap();
+    assert_eq!(
+        values,
+        vec![
+            None,
+            Some(("b1".to_string(), "v1".as_bytes().to_vec())),
+            None
+        ]
+    );
+}
diff --git a/crates/test-programs/src/bin/preview1_poll_oneoff_stdio.rs b/crates/test-programs/src/bin/preview1_poll_oneoff_stdio.rs
index bb3216b57..3ce9f75f2 100644
--- a/crates/test-programs/src/bin/preview1_poll_oneoff_stdio.rs
+++ b/crates/test-programs/src/bin/preview1_poll_oneoff_stdio.rs
@@ -108,7 +108,7 @@ unsafe fn test_stdout_stderr_write() {
             panic!("timed out with the following pending subs: {:?}", writable)
         }
         let mut subs = writable_subs(&writable);
-        subs.push(clock.clone());
+        subs.push(clock);
         let out = poll_oneoff_impl(&subs).unwrap();
         for event in out {
             match event.userdata {
diff --git a/crates/test-programs/src/lib.rs b/crates/test-programs/src/lib.rs
index d4be5399d..3586b7102 100644
--- a/crates/test-programs/src/lib.rs
+++ b/crates/test-programs/src/lib.rs
@@ -37,3 +37,11 @@ pub mod config {
         world: "wasi:config/imports",
     });
 }
+
+pub mod keyvalue {
+    wit_bindgen::generate!({
+        path: "../wasi-keyvalue/wit",
+        world: "wasi:keyvalue/imports",
+        type_section_suffix: "keyvalue",
+    });
+}
diff --git a/crates/wasi-keyvalue/tests/main.rs b/crates/wasi-keyvalue/tests/main.rs
new file mode 100644
index 000000000..fa103d63e
--- /dev/null
+++ b/crates/wasi-keyvalue/tests/main.rs
@@ -0,0 +1,93 @@
+use anyhow::{anyhow, Result};
+use test_programs_artifacts::{foreach_keyvalue, KEYVALUE_MAIN_COMPONENT};
+use wasmtime::{
+    component::{Component, Linker, ResourceTable},
+    Store,
+};
+use wasmtime_wasi::{bindings::Command, WasiCtx, WasiCtxBuilder, WasiView};
+use wasmtime_wasi_keyvalue::{WasiKeyValue, WasiKeyValueCtx, WasiKeyValueCtxBuilder};
+
+struct Ctx {
+    table: ResourceTable,
+    wasi_ctx: WasiCtx,
+    wasi_keyvalue_ctx: WasiKeyValueCtx,
+}
+
+impl WasiView for Ctx {
+    fn table(&mut self) -> &mut ResourceTable {
+        &mut self.table
+    }
+
+    fn ctx(&mut self) -> &mut WasiCtx {
+        &mut self.wasi_ctx
+    }
+}
+
+async fn run_wasi(path: &str, ctx: Ctx) -> Result<()> {
+    let engine = test_programs_artifacts::engine(|config| {
+        config.async_support(true);
+    });
+    let mut store = Store::new(&engine, ctx);
+    let component = Component::from_file(&engine, path)?;
+
+    let mut linker = Linker::new(&engine);
+    wasmtime_wasi::add_to_linker_async(&mut linker)?;
+    wasmtime_wasi_keyvalue::add_to_linker(&mut linker, |h: &mut Ctx| {
+        WasiKeyValue::new(&h.wasi_keyvalue_ctx, &mut h.table)
+    })?;
+
+    let command = Command::instantiate_async(&mut store, &component, &linker).await?;
+    command
+        .wasi_cli_run()
+        .call_run(&mut store)
+        .await?
+        .map_err(|()| anyhow!("command returned with failing exit status"))
+}
+
+macro_rules! assert_test_exists {
+    ($name:ident) => {
+        #[allow(unused_imports)]
+        use self::$name as _;
+    };
+}
+
+foreach_keyvalue!(assert_test_exists);
+
+#[tokio::test(flavor = "multi_thread")]
+async fn keyvalue_main() -> Result<()> {
+    run_wasi(
+        KEYVALUE_MAIN_COMPONENT,
+        Ctx {
+            table: ResourceTable::new(),
+            wasi_ctx: WasiCtxBuilder::new()
+                .inherit_stderr()
+                .env("IDENTIFIER", "")
+                .build(),
+            wasi_keyvalue_ctx: WasiKeyValueCtxBuilder::new()
+                .in_memory_data([("atomics_key", "5")])
+                .build(),
+        },
+    )
+    .await
+}
+
+#[cfg(feature = "redis")]
+#[tokio::test(flavor = "multi_thread")]
+async fn keyvalue_redis() -> Result<()> {
+    run_wasi(
+        KEYVALUE_MAIN_COMPONENT,
+        Ctx {
+            table: ResourceTable::new(),
+            wasi_ctx: WasiCtxBuilder::new()
+                .inherit_stderr()
+                .env("IDENTIFIER", "redis://127.0.0.1/")
+                .build(),
+            wasi_keyvalue_ctx: WasiKeyValueCtxBuilder::new()
+                .allow_redis_hosts(&["127.0.0.1"])
+                .redis_connection_timeout(std::time::Duration::from_secs(5))
+                .redis_response_timeout(std::time::Duration::from_secs(5))
+                .build(),
+        },
+    )
+    .await
+}