Repository: lima-vm/lima

Issue:
Consider setting `CONFIG_GUESTAGENT_ARCH_{ARMV7L, RISCV64, S390X, ...}` to `n` (for Homebrew)


`armv7l`, `riscv64`, and `s390x` (being added via #3319) are very rarely used and should not waste the disk space by default:
```console
$ ls -lh ~/.local/share/lima/lima-guestagent.Linux-*
-rw-r--r--  1 suda  staff    53M  3  9 05:04 /Users/suda/.local/share/lima/lima-guestagent.Linux-aarch64
-rw-r--r--  1 suda  staff    52M  3  9 05:04 /Users/suda/.local/share/lima/lima-guestagent.Linux-armv7l
-rw-r--r--  1 suda  staff    53M  3  9 05:04 /Users/suda/.local/share/lima/lima-guestagent.Linux-riscv64
-rw-r--r--  1 suda  staff    58M  3  9 05:04 /Users/suda/.local/share/lima/lima-guestagent.Linux-s390x
-rw-r--r--  1 suda  staff    55M  3  9 05:04 /Users/suda/.local/share/lima/lima-guestagent.Linux-x86_64
```

My suggestion is:

 Dist  | Config
-------|-------
Source distribution | Just compile `aarch64` and `x86_64` by default. Probably, there should be a script like `./configure --guestagent-arch=all` to allow keeping compiling everything.
Homebrew | Split the `lima` formula to `lima` and `lima-extra-guestagents`. Needs to be negotiated with Homebrew maintainers.
Binary distribution in https://github.com/lima-vm/lima/releases | Should follow the same convention as Homebrew?


The same should apply to other potential guestagents in future too (`ppc64le`, `FreeBSD-x86_64`, etc.)


Hints from issue comments:
@afbjorklund: > Probably, there should be a script like ./configure --guestagent-arch=all to allow keeping compiling everything.

Normally (with konfig) this is handled by providing different config files.

You can even keep the defaults as they are, and only change `config.mk`

config.mk
```
CONFIG_GUESTAGENT_ARCH_X8664=y
CONFIG_GUESTAGENT_ARCH_AARCH64=y
CONFIG_GUESTAGENT_ARCH_ARMV7L=n
CONFIG_GUESTAGENT_ARCH_RISCV64=n
CONFIG_GUESTAGENT_ARCH_S390X=n
```

----

defconfig (`kconfig-conf --savedefconfig defconfig Kconfig`)
```
# CONFIG_GUESTAGENT_ARCH_ARMV7L is not set
# CONFIG_GUESTAGENT_ARCH_RISCV64 is not set
# CONFIG_GUESTAGENT_ARCH_S390X is not set
```

alldefconfig (`kconfig-conf --alldefconfig Kconfig`)
```
CONFIG_GUESTAGENT_ARCH_X8664=y
CONFIG_GUESTAGENT_ARCH_AARCH64=y
CONFIG_GUESTAGENT_ARCH_ARMV7L=y
CONFIG_GUESTAGENT_ARCH_RISCV64=y
CONFIG_GUESTAGENT_ARCH_S390X=y
```
@jandubois: I'm confused; can't we already build whatever we want with a combination of these:

https://github.com/lima-vm/lima/blob/4a88b71430e37562cad05722a1c5697ae144fab4/Makefile#L97-L101

Personally I always thought that the `kconfig` mechanism is kind of weird.@afbjorklund: > Personally I always thought that the `kconfig` mechanism is kind of weird.

I think at this point, the makefile targets could probably replace the Kconfig?

It's a bit doubled up, and having dedicated targets seemed to be preferred.

I mostly used it (as an alternative to autoconf), because **I** was familar with it...@AkihiroSuda: > I think at this point, the makefile targets could probably replace the Kconfig?

👍 @AkihiroSuda: @lima-vm/maintainers 

Are we safe to do this in v1.1, or do we want to revisit this in v1.2?


> 
> Dist | Config
> -- | --
> Source distribution | Just compile aarch64 and x86_64 by default. Probably, there should be a script like ./configure --guestagent-arch=all to allow keeping compiling everything.
> Homebrew | Split the lima formula to lima and lima-extra-guestagents. Needs to be negotiated with Homebrew maintainers.
> Binary distribution in https://github.com/lima-vm/lima/releases | Should follow the same convention as Homebrew?
> 
> @jandubois: I don't like the idea of an additional configure script. I think we should just add an `all-guestagents` target, and redefine `guestagents` to mean `aarch64` and `x86_64` unconditionally.

For Homebrew it makes sense to me to just include `aarch64` and `x86_64` guestagents. I don't think the additional guestagents need to be packaged, but it is up to the Homebrew maintainers.

For the binary distribution I don't mind including all guestagents. I don't know who is using the binary releases, but the download numbers are small compared to Homebrew installs:

```console
$ gh api repos/lima-vm/lima/releases --jq '
  [ ["Tag", "Asset", "Downloads"] ] +
  (map( .tag_name as $tag | .assets | map([ $tag, .name, .download_count ]) ) | add)
  | .[] | @tsv' | grep -vE '(alpha|beta|go-mod|SHA)' | column -s $'\t' -t
Tag      Asset                             Downloads
v1.0.7   lima-1.0.7-Darwin-arm64.tar.gz    1052
v1.0.7   lima-1.0.7-Darwin-x86_64.tar.gz   1547
v1.0.7   lima-1.0.7-Linux-aarch64.tar.gz   35
v1.0.7   lima-1.0.7-Linux-x86_64.tar.gz    5172
v1.0.6   lima-1.0.6-Darwin-arm64.tar.gz    1287
v1.0.6   lima-1.0.6-Darwin-x86_64.tar.gz   2225
v1.0.6   lima-1.0.6-Linux-aarch64.tar.gz   84
v1.0.6   lima-1.0.6-Linux-x86_64.tar.gz    6059
…

❯ brew info lima
==> lima: stable 1.0.7 (bottled), HEAD
…
==> Analytics
install: 14,704 (30 days), 51,154 (90 days), 192,827 (365 days)
install-on-request: 7,899 (30 days), 28,141 (90 days), 122,289 (365 days)
```

I just checked `brew info lima` on Intel, and the Analytics are the same, so these look like combined platform numbers.

Solution patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c0c5d6e0..e3ce40ca 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,6 +13,7 @@ on:
     - 'master'
 env:
   GO111MODULE: on
+  GOTOOLCHAIN: local
 permissions:
   contents: read
 
@@ -37,7 +38,7 @@ jobs:
     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
       with:
         fetch-depth: 1
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - name: Make darwin artifacts
@@ -64,7 +65,7 @@ jobs:
       with:
         name: artifacts-darwin
         path: _artifacts/
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - name: Install gcc-x86-64-linux-gnu
@@ -79,6 +80,8 @@ jobs:
       run: make artifacts-linux
     - name: "Make misc artifacts"
       run: make artifacts-misc
+    - name: "Validate artifactts"
+      run: ./hack/validate-artifact.sh ./_artifacts/*.tar.gz
     - name: "SHA256SUMS"
       run: |
         ( cd _artifacts; sha256sum *.tar.gz ) | tee /tmp/SHA256SUMS
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 66791bb2..1bdc0ab6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,6 +16,7 @@ on:
     - "**.md"
 env:
   LIMACTL_CREATE_ARGS: ""
+  GOTOOLCHAIN: local
 
 permissions: read-all
 
@@ -29,7 +30,7 @@ jobs:
       with:
         # To avoid "failed to load YAML file \"templates/experimental/riscv64.yaml\": can't parse builtin Lima version \"3f3a6f6\": 3f3a6f6 is not in dotted-tri format"
         fetch-depth: 0
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - name: Install protoc
@@ -111,7 +112,7 @@ jobs:
     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
       with:
         fetch-depth: 1
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: ${{ matrix.go-version }}
     - name: Unit tests
@@ -157,7 +158,7 @@ jobs:
     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
       with:
         fetch-depth: 1
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - name: Unit tests
@@ -184,7 +185,7 @@ jobs:
       with:
         # To avoid "failed to load YAML file \"templates/experimental/riscv64.yaml\": can't parse builtin Lima version \"3f3a6f6\": 3f3a6f6 is not in dotted-tri format"
         fetch-depth: 0
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - name: Unit tests
@@ -259,7 +260,7 @@ jobs:
       with:
         # To avoid "failed to load YAML file \"templates/experimental/riscv64.yaml\": can't parse builtin Lima version \"3f3a6f6\": 3f3a6f6 is not in dotted-tri format"
         fetch-depth: 0
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - name: Make
@@ -310,7 +311,7 @@ jobs:
         # fetch-depth is set to 0 to let `limactl --version` print semver-ish version
         fetch-depth: 0
         ref: ${{ github.event.pull_request.head.sha }}
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684  # v4.2.3
@@ -355,7 +356,7 @@ jobs:
     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
       with:
         fetch-depth: 0
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - name: Make
@@ -419,7 +420,7 @@ jobs:
         path: homebrew-core
         fetch-depth: 0
         filter: tree:0
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - name: Cache image used by ${{ matrix.oldver }}/examples/ubuntu-lts.yaml
@@ -455,7 +456,7 @@ jobs:
     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
       with:
         fetch-depth: 0
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - name: Make
@@ -490,7 +491,7 @@ jobs:
     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
       with:
         fetch-depth: 0
-    - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b  # v5.4.0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
       with:
         go-version: 1.24.x
     - name: Install gomodjail
@@ -508,3 +509,49 @@ jobs:
         template: templates/default.yaml
     - name: Smoke test
       run: gomodjail run --go-mod=./go.mod -- limactl start --tty=false
+
+  cross:
+    name: "Cross-compile (NetBSD, DragonFlyBSD)"
+    runs-on: ubuntu-24.04
+    timeout-minutes: 30
+    steps:
+    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
+      with:
+        # To avoid "failed to load YAML file \"templates/experimental/riscv64.yaml\": can't parse builtin Lima version \"3f3a6f6\": 3f3a6f6 is not in dotted-tri format"
+        fetch-depth: 0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
+      with:
+        go-version: 1.24.x
+    - run: GOOS=netbsd go build ./...
+    - run: GOOS=dragonfly go build ./...
+
+  qemu-linux-old:
+    name: "Smoke tests (QEMU, old Linux host)"
+    runs-on: ubuntu-22.04  # QEMU 6.2
+    timeout-minutes: 30
+    steps:
+    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
+      with:
+        # To avoid "failed to load YAML file \"templates/experimental/riscv64.yaml\": can't parse builtin Lima version \"3f3a6f6\": 3f3a6f6 is not in dotted-tri format"
+        fetch-depth: 0
+    - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
+      with:
+        go-version: 1.24.x
+    - name: Make
+      run: make
+    - name: Install
+      run: sudo make install
+    - name: Cache image used by templates/default.yaml
+      uses: ./.github/actions/setup_cache_for_template
+      with:
+        template: templates/default.yaml
+    - name: Install test dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y --no-install-recommends ovmf qemu-system-x86 qemu-utils
+        qemu-system-x86_64 --version
+        sudo modprobe kvm
+        # `sudo usermod -aG kvm $(whoami)` does not take an effect on GHA
+        sudo chown $(whoami) /dev/kvm
+    - name: Smoke test
+      run: limactl start --tty=false
diff --git a/Kconfig b/Kconfig
index d52d6fb0..7e40d787 100644
--- a/Kconfig
+++ b/Kconfig
@@ -47,4 +47,4 @@ config GUESTAGENT_COMPRESS
 	bool "guestagent compress"
 	help
 		Compress lima-guestagent
-	default n
+	default y
diff --git a/Makefile b/Makefile
index 5f679955..a6aaea1e 100644
--- a/Makefile
+++ b/Makefile
@@ -140,7 +140,7 @@ exe: _output/bin/limactl$(exe)
 
 .PHONY: minimal native
 minimal: clean limactl native-guestagent default_template
-native: clean limactl helpers native-guestagent templates
+native: clean limactl helpers native-guestagent templates template_experimentals
 
 ################################################################################
 # Kconfig
@@ -291,7 +291,7 @@ ALL_GUESTAGENTS = $(addsuffix $(gz),$(ALL_GUESTAGENTS_NOT_COMPRESSED))
 guestagent_path = $(foreach arch,$(2),$($(1)_GUESTAGENT_PATH_COMMON)$(arch)$(gz))
 
 ifeq ($(CONFIG_GUESTAGENT_OS_LINUX),y)
-NATIVE_GUESTAGENT_ARCH = $(shell uname -m | sed -e s/arm64/aarch64/)
+NATIVE_GUESTAGENT_ARCH = $(shell echo $(GOARCH) | sed -e s/arm64/aarch64/ -e s/arm/armv7l/ -e s/amd64/x86_64/)
 NATIVE_GUESTAGENT = $(call guestagent_path,LINUX,$(NATIVE_GUESTAGENT_ARCH))
 ADDITIONAL_GUESTAGENT_ARCHS = $(filter-out $(NATIVE_GUESTAGENT_ARCH),$(LINUX_GUESTAGENT_ARCHS))
 ADDITIONAL_GUESTAGENTS = $(call guestagent_path,LINUX,$(ADDITIONAL_GUESTAGENT_ARCHS))
@@ -436,14 +436,15 @@ ifeq ($(native_compiling),true)
 	$< tmpl copy --embed-all templates/default.yaml $@
 endif
 
-schema-limayaml.json: _output/bin/limactl$(exe) default-template.yaml
+schema-limayaml.json: _output/bin/limactl$(exe) templates/default.yaml default-template.yaml
 ifeq ($(native_compiling),true)
-	$< generate-jsonschema --schemafile $@ default-template.yaml
+	# validate both the original template (with the "base" etc), and the embedded template
+	$< generate-jsonschema --schemafile $@ templates/default.yaml default-template.yaml
 endif
 
 .PHONY: check-jsonschema
-check-jsonschema: schema-limayaml.json default-template.yaml
-	check-jsonschema --schemafile schema-limayaml.json default-template.yaml
+check-jsonschema: schema-limayaml.json templates/default.yaml default-template.yaml
+	check-jsonschema --schemafile schema-limayaml.json templates/default.yaml default-template.yaml
 
 ################################################################################
 .PHONY: diagrams
@@ -509,6 +510,7 @@ generate:
 
 ################################################################################
 # _artifacts/lima-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M)
+# _artifacts/lima-additional-guestagents-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M)
 .PHONY: artifact
 
 # returns the capitalized string of $(1).
@@ -534,11 +536,13 @@ endif
 ARTIFACT_OS = $(call capitalize,$(GOOS))
 ARTIFACT_UNAME_M = $(call to_uname_m,$(GOARCH))
 ARTIFACT_PATH_COMMON = _artifacts/lima-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M)
+ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON = _artifacts/lima-additional-guestagents-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M)
 
-artifact: $(addprefix $(ARTIFACT_PATH_COMMON),$(ARTIFACT_FILE_EXTENSIONS))
+artifact: $(addprefix $(ARTIFACT_PATH_COMMON),$(ARTIFACT_FILE_EXTENSIONS)) \
+	$(addprefix $(ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON),$(ARTIFACT_FILE_EXTENSIONS))
 
 ARTIFACT_DES =  _output/bin/limactl$(exe) $(LIMA_DEPS) $(HELPERS_DEPS) \
-	$(ALL_GUESTAGENTS) \
+	$(NATIVE_GUESTAGENT) \
 	$(TEMPLATES) $(TEMPLATE_EXPERIMENTALS) \
 	$(DOCUMENTATION) _output/share/doc/lima/templates \
 	_output/share/man/man1/limactl.1
@@ -547,9 +551,18 @@ ARTIFACT_DES =  _output/bin/limactl$(exe) $(LIMA_DEPS) $(HELPERS_DEPS) \
 $(ARTIFACT_PATH_COMMON).tar.gz: $(ARTIFACT_DES) | _artifacts
 	$(TAR) -C _output/ --no-xattrs -czvf $@ ./
 
+$(ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON).tar.gz:
+	# FIXME: do not exec make from make
+	make clean additional-guestagents
+	$(TAR) -C _output/ --no-xattrs -czvf $@ ./
+
 $(ARTIFACT_PATH_COMMON).zip: $(ARTIFACT_DES) | _artifacts
 	cd _output && $(ZIP) -r ../$@ *
 
+$(ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON).zip:
+	make clean additional-guestagents
+	cd _output && $(ZIP) -r ../$@ *
+
 # generate manpages using native limactl.
 manpages-using-native-limactl: GOOS = $(GOHOSTOS)
 manpages-using-native-limactl: GOARCH = $(GOHOSTARCH)
@@ -581,7 +594,7 @@ artifact-%-arm64 artifact-%-aarch64 artifact-arm64 artifact-aarch64: GOARCH = ar
 
 # build cross arch binaries.
 artifact-%: $$(call generate_manpages_if_needed)
-	make artifact GOOS=$(GOOS) GOARCH=$(GOARCH)
+	make clean artifact GOOS=$(GOOS) GOARCH=$(GOARCH)
 
 .PHONY: artifacts-misc
 artifacts-misc: | _artifacts
diff --git a/cmd/limactl/disk.go b/cmd/limactl/disk.go
index f5d17136..db9788c4 100644
--- a/cmd/limactl/disk.go
+++ b/cmd/limactl/disk.go
@@ -16,7 +16,7 @@ import (
 	"github.com/docker/go-units"
 	"github.com/lima-vm/go-qcow2reader"
 	"github.com/lima-vm/lima/pkg/nativeimgutil"
-	"github.com/lima-vm/lima/pkg/qemu"
+	"github.com/lima-vm/lima/pkg/qemu/imgutil"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/sirupsen/logrus"
@@ -111,10 +111,11 @@ func diskCreateAction(cmd *cobra.Command, args []string) error {
 	}
 
 	// qemu may not be available, use it only if needed.
+	dataDisk := filepath.Join(diskDir, filenames.DataDisk)
 	if format == "raw" {
-		err = nativeimgutil.CreateRawDataDisk(diskDir, int(diskSize))
+		err = nativeimgutil.CreateRawDisk(dataDisk, int(diskSize))
 	} else {
-		err = qemu.CreateDataDisk(diskDir, format, int(diskSize))
+		err = imgutil.CreateDisk(dataDisk, format, int(diskSize))
 	}
 	if err != nil {
 		rerr := os.RemoveAll(diskDir)
@@ -407,10 +408,11 @@ func diskResizeAction(cmd *cobra.Command, args []string) error {
 	}
 
 	// qemu may not be available, use it only if needed.
+	dataDisk := filepath.Join(disk.Dir, filenames.DataDisk)
 	if disk.Format == "raw" {
-		err = nativeimgutil.ResizeRawDataDisk(disk.Dir, int(diskSize))
+		err = nativeimgutil.ResizeRawDisk(dataDisk, int(diskSize))
 	} else {
-		err = qemu.ResizeDataDisk(disk.Dir, disk.Format, int(diskSize))
+		err = imgutil.ResizeDisk(dataDisk, disk.Format, int(diskSize))
 	}
 	if err != nil {
 		return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go
index e9d33311..b24e6141 100644
--- a/cmd/limactl/edit.go
+++ b/cmd/limactl/edit.go
@@ -133,6 +133,7 @@ func editAction(cmd *cobra.Command, args []string) error {
 	if err := os.WriteFile(filePath, yBytes, 0o644); err != nil {
 		return err
 	}
+
 	if inst != nil {
 		logrus.Infof("Instance %q configuration edited", inst.Name)
 	}
@@ -157,6 +158,13 @@ func editAction(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
+
+	// store.Inspect() syncs values between inst.YAML and the store.
+	// This call applies the validated template to the store.
+	inst, err = store.Inspect(inst.Name)
+	if err != nil {
+		return err
+	}
 	return instance.Start(ctx, inst, "", false)
 }
 
diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go
index 3fad84f7..813b263a 100644
--- a/cmd/limactl/editflags/editflags.go
+++ b/cmd/limactl/editflags/editflags.go
@@ -66,6 +66,11 @@ func registerEdit(cmd *cobra.Command, commentPrefix string) {
 
 	// negative performance impact: https://gitlab.com/qemu-project/qemu/-/issues/334
 	flags.Bool("video", false, commentPrefix+"Enable video output (has negative performance impact for QEMU)")
+
+	flags.Float32("disk", 0, commentPrefix+"Disk size in GiB") // colima-compatible
+	_ = cmd.RegisterFlagCompletionFunc("disk", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
+		return []string{"10", "30", "50", "100", "200"}, cobra.ShellCompDirectiveNoFileComp
+	})
 }
 
 // RegisterCreate registers flags related to in-place YAML modification, for `limactl create`.
@@ -83,11 +88,6 @@ func RegisterCreate(cmd *cobra.Command, commentPrefix string) {
 		return []string{"user", "system", "user+system", "none"}, cobra.ShellCompDirectiveNoFileComp
 	})
 
-	flags.Float32("disk", 0, commentPrefix+"Disk size in GiB") // colima-compatible
-	_ = cmd.RegisterFlagCompletionFunc("memory", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
-		return []string{"10", "30", "50", "100", "200"}, cobra.ShellCompDirectiveNoFileComp
-	})
-
 	flags.String("vm-type", "", commentPrefix+"Virtual machine type (qemu, vz)") // colima-compatible
 	_ = cmd.RegisterFlagCompletionFunc("vm-type", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
 		return []string{"qemu", "vz"}, cobra.ShellCompDirectiveNoFileComp
@@ -241,7 +241,7 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
 			true,
 			false,
 		},
-		{"disk", d(".disk= \"%sGiB\""), true, false},
+		{"disk", d(".disk= \"%sGiB\""), false, false},
 		{"vm-type", d(".vmType = %q"), true, false},
 		{"plain", d(".plain = %s"), true, false},
 	}
diff --git a/cmd/limactl/genschema.go b/cmd/limactl/genschema.go
index 8de619fd..d1be43ae 100644
--- a/cmd/limactl/genschema.go
+++ b/cmd/limactl/genschema.go
@@ -9,10 +9,9 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/goccy/go-yaml"
 	"github.com/invopop/jsonschema"
+	"github.com/lima-vm/lima/pkg/jsonschemautil"
 	"github.com/lima-vm/lima/pkg/limayaml"
-	jsonschema2 "github.com/santhosh-tekuri/jsonschema/v6"
 	"github.com/spf13/cobra"
 	orderedmap "github.com/wk8/go-ordered-map/v2"
 
@@ -60,6 +59,18 @@ func genschemaAction(cmd *cobra.Command, args []string) error {
 		{Type: "string"},
 		{Type: "object"},
 	}
+	// allow BaseTemplates to be either string (url) or array (array)
+	schema.Definitions["BaseTemplates"].Type = "" // was: "array"
+	schema.Definitions["BaseTemplates"].OneOf = []*jsonschema.Schema{
+		{Type: "string"},
+		{Type: "array"},
+	}
+	// allow LocatorWithDigest to be either string (url) or object (struct)
+	schema.Definitions["LocatorWithDigest"].Type = "" // was: "object"
+	schema.Definitions["LocatorWithDigest"].OneOf = []*jsonschema.Schema{
+		{Type: "string"},
+		{Type: "object"},
+	}
 	properties := schema.Definitions["LimaYAML"].Properties
 	getProp(properties, "os").Enum = toAny(limayaml.OSTypes)
 	getProp(properties, "arch").Enum = toAny(limayaml.ArchTypes)
@@ -81,22 +92,8 @@ func genschemaAction(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	compiler := jsonschema2.NewCompiler()
-	schema2, err := compiler.Compile(file)
-	if err != nil {
-		return err
-	}
 	for _, f := range args {
-		b, err := os.ReadFile(f)
-		if err != nil {
-			return err
-		}
-		var y any
-		err = yaml.Unmarshal(b, &y)
-		if err != nil {
-			return err
-		}
-		err = schema2.Validate(y)
+		err = jsonschemautil.Validate(file, f)
 		if err != nil {
 			return fmt.Errorf("%q: %w", f, err)
 		}
diff --git a/cmd/limactl/hostagent.go b/cmd/limactl/hostagent.go
index f7606ff0..ae02207d 100644
--- a/cmd/limactl/hostagent.go
+++ b/cmd/limactl/hostagent.go
@@ -32,6 +32,7 @@ func newHostagentCommand() *cobra.Command {
 	hostagentCommand.Flags().StringP("pidfile", "p", "", "Write PID to file")
 	hostagentCommand.Flags().String("socket", "", "Path of hostagent socket")
 	hostagentCommand.Flags().Bool("run-gui", false, "Run GUI synchronously within hostagent")
+	hostagentCommand.Flags().String("guestagent", "", "Local file path (not URL) of lima-guestagent.OS-ARCH[.gz]")
 	hostagentCommand.Flags().String("nerdctl-archive", "", "Local file path (not URL) of nerdctl-full-VERSION-GOOS-GOARCH.tar.gz")
 	return hostagentCommand
 }
@@ -78,6 +79,13 @@ func hostagentAction(cmd *cobra.Command, args []string) error {
 
 	initLogrus(stderr)
 	var opts []hostagent.Opt
+	guestagentBinary, err := cmd.Flags().GetString("guestagent")
+	if err != nil {
+		return err
+	}
+	if guestagentBinary != "" {
+		opts = append(opts, hostagent.WithGuestAgentBinary(guestagentBinary))
+	}
 	nerdctlArchive, err := cmd.Flags().GetString("nerdctl-archive")
 	if err != nil {
 		return err
diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go
index a428a9fd..d56047f5 100644
--- a/cmd/limactl/main.go
+++ b/cmd/limactl/main.go
@@ -186,10 +186,9 @@ func newApp() *cobra.Command {
 		newTunnelCommand(),
 		newTemplateCommand(),
 		newRestartCommand(),
+		newSudoersCommand(),
+		newStartAtLoginCommand(),
 	)
-	for _, cmd := range additionalAdvancedCommands() {
-		rootCmd.AddCommand(cmd)
-	}
 
 	return rootCmd
 }
diff --git a/cmd/limactl/main_darwin.go b/cmd/limactl/main_darwin.go
deleted file mode 100644
index f3849d20..00000000
--- a/cmd/limactl/main_darwin.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// SPDX-FileCopyrightText: Copyright The Lima Authors
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-import "github.com/spf13/cobra"
-
-func additionalAdvancedCommands() []*cobra.Command {
-	return []*cobra.Command{
-		newSudoersCommand(),
-		startAtLoginCommand(),
-	}
-}
diff --git a/cmd/limactl/main_linux.go b/cmd/limactl/main_linux.go
deleted file mode 100644
index f09c3ea7..00000000
--- a/cmd/limactl/main_linux.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// SPDX-FileCopyrightText: Copyright The Lima Authors
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-import "github.com/spf13/cobra"
-
-func additionalAdvancedCommands() []*cobra.Command {
-	return []*cobra.Command{
-		startAtLoginCommand(),
-	}
-}
diff --git a/cmd/limactl/main_windows.go b/cmd/limactl/main_windows.go
deleted file mode 100644
index bf3bba33..00000000
--- a/cmd/limactl/main_windows.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// SPDX-FileCopyrightText: Copyright The Lima Authors
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-import "github.com/spf13/cobra"
-
-func additionalAdvancedCommands() []*cobra.Command {
-	return nil
-}
diff --git a/cmd/limactl/start-at-login.go b/cmd/limactl/start-at-login.go
new file mode 100644
index 00000000..b7ca2aaf
--- /dev/null
+++ b/cmd/limactl/start-at-login.go
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"github.com/spf13/cobra"
+)
+
+func newStartAtLoginCommand() *cobra.Command {
+	startAtLoginCommand := &cobra.Command{
+		Use:               "start-at-login INSTANCE",
+		Short:             "Register/Unregister an autostart file for the instance",
+		Args:              WrapArgsError(cobra.MaximumNArgs(1)),
+		RunE:              startAtLoginAction,
+		ValidArgsFunction: startAtLoginComplete,
+		GroupID:           advancedCommand,
+	}
+
+	startAtLoginCommand.Flags().Bool(
+		"enabled", true,
+		"Automatically start the instance when the user logs in",
+	)
+
+	return startAtLoginCommand
+}
+
+func startAtLoginComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+	return bashCompleteInstanceNames(cmd)
+}
diff --git a/cmd/limactl/start-at-login_unix.go b/cmd/limactl/start-at-login_unix.go
index b6e0e0ae..6b4b189d 100644
--- a/cmd/limactl/start-at-login_unix.go
+++ b/cmd/limactl/start-at-login_unix.go
@@ -16,24 +16,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func startAtLoginCommand() *cobra.Command {
-	startAtLoginCommand := &cobra.Command{
-		Use:               "start-at-login INSTANCE",
-		Short:             "Register/Unregister an autostart file for the instance",
-		Args:              WrapArgsError(cobra.MaximumNArgs(1)),
-		RunE:              startAtLoginAction,
-		ValidArgsFunction: startAtLoginComplete,
-		GroupID:           advancedCommand,
-	}
-
-	startAtLoginCommand.Flags().Bool(
-		"enabled", true,
-		"Automatically start the instance when the user logs in",
-	)
-
-	return startAtLoginCommand
-}
-
 func startAtLoginAction(cmd *cobra.Command, args []string) error {
 	instName := DefaultInstanceName
 	if len(args) > 0 {
@@ -71,7 +53,3 @@ func startAtLoginAction(cmd *cobra.Command, args []string) error {
 
 	return nil
 }
-
-func startAtLoginComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
-	return bashCompleteInstanceNames(cmd)
-}
diff --git a/cmd/limactl/start-at-login_windows.go b/cmd/limactl/start-at-login_windows.go
new file mode 100644
index 00000000..5112dff8
--- /dev/null
+++ b/cmd/limactl/start-at-login_windows.go
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"errors"
+
+	"github.com/spf13/cobra"
+)
+
+func startAtLoginAction(_ *cobra.Command, _ []string) error {
+	return errors.New("start-at-login command is only supported on macOS and Linux right now")
+}
diff --git a/cmd/limactl/sudoers.go b/cmd/limactl/sudoers.go
new file mode 100644
index 00000000..7478312e
--- /dev/null
+++ b/cmd/limactl/sudoers.go
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"fmt"
+
+	"github.com/lima-vm/lima/pkg/networks"
+	"github.com/spf13/cobra"
+)
+
+const socketVMNetURL = "https://lima-vm.io/docs/config/network/vmnet/#socket_vmnet"
+
+// newSudoersCommand is specific to macOS, but the help message is
+// compiled on Linux too, as depended by `make docsy`.
+// https://github.com/lima-vm/lima/issues/3436
+func newSudoersCommand() *cobra.Command {
+	sudoersCommand := &cobra.Command{
+		Use: "sudoers [--check [SUDOERSFILE-TO-CHECK]]",
+		Example: `
+To generate the /etc/sudoers.d/lima file:
+$ limactl sudoers | sudo tee /etc/sudoers.d/lima
+
+To validate the existing /etc/sudoers.d/lima file:
+$ limactl sudoers --check /etc/sudoers.d/lima
+`,
+		Short: "Generate the content of the /etc/sudoers.d/lima file",
+		Long: fmt.Sprintf(`Generate the content of the /etc/sudoers.d/lima file for enabling vmnet.framework support (socket_vmnet) on macOS.
+The content is written to stdout, NOT to the file.
+This command must not run as the root user.
+See %s for the usage.`, socketVMNetURL),
+		Args:    WrapArgsError(cobra.MaximumNArgs(1)),
+		RunE:    sudoersAction,
+		GroupID: advancedCommand,
+	}
+	cfgFile, _ := networks.ConfigFile()
+	sudoersCommand.Flags().Bool("check", false,
+		fmt.Sprintf("check that the sudoers file is up-to-date with %q", cfgFile))
+	return sudoersCommand
+}
diff --git a/cmd/limactl/sudoers_darwin.go b/cmd/limactl/sudoers_darwin.go
index dfe3ba70..96fb265b 100644
--- a/cmd/limactl/sudoers_darwin.go
+++ b/cmd/limactl/sudoers_darwin.go
@@ -13,33 +13,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-const networksURL = "https://lima-vm.io/docs/config/network/#socket_vmnet"
-
-func newSudoersCommand() *cobra.Command {
-	sudoersCommand := &cobra.Command{
-		Use: "sudoers [--check [SUDOERSFILE-TO-CHECK]]",
-		Example: `
-To generate the /etc/sudoers.d/lima file:
-$ limactl sudoers | sudo tee /etc/sudoers.d/lima
-
-To validate the existing /etc/sudoers.d/lima file:
-$ limactl sudoers --check /etc/sudoers.d/lima
-`,
-		Short: "Generate the content of the /etc/sudoers.d/lima file",
-		Long: fmt.Sprintf(`Generate the content of the /etc/sudoers.d/lima file for enabling vmnet.framework support.
-The content is written to stdout, NOT to the file.
-This command must not run as the root user.
-See %s for the usage.`, networksURL),
-		Args:    WrapArgsError(cobra.MaximumNArgs(1)),
-		RunE:    sudoersAction,
-		GroupID: advancedCommand,
-	}
-	cfgFile, _ := networks.ConfigFile()
-	sudoersCommand.Flags().Bool("check", false,
-		fmt.Sprintf("check that the sudoers file is up-to-date with %q", cfgFile))
-	return sudoersCommand
-}
-
 func sudoersAction(cmd *cobra.Command, args []string) error {
 	nwCfg, err := networks.LoadConfig()
 	if err != nil {
@@ -47,7 +20,7 @@ func sudoersAction(cmd *cobra.Command, args []string) error {
 	}
 	// Make sure the current network configuration is secure
 	if err := nwCfg.Validate(); err != nil {
-		logrus.Infof("Please check %s for more information.", networksURL)
+		logrus.Infof("Please check %s for more information.", socketVMNetURL)
 		return err
 	}
 	check, err := cmd.Flags().GetBool("check")
diff --git a/cmd/limactl/sudoers_nodarwin.go b/cmd/limactl/sudoers_nodarwin.go
new file mode 100644
index 00000000..07f60a50
--- /dev/null
+++ b/cmd/limactl/sudoers_nodarwin.go
@@ -0,0 +1,16 @@
+//go:build !darwin
+
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"errors"
+
+	"github.com/spf13/cobra"
+)
+
+func sudoersAction(_ *cobra.Command, _ []string) error {
+	return errors.New("sudoers command is only supported on macOS right now")
+}
diff --git a/config.mk b/config.mk
index 9a02aae2..06ab7787 100644
--- a/config.mk
+++ b/config.mk
@@ -5,4 +5,4 @@ CONFIG_GUESTAGENT_ARCH_ARMV7L=y
 CONFIG_GUESTAGENT_ARCH_PPC64LE=y
 CONFIG_GUESTAGENT_ARCH_RISCV64=y
 CONFIG_GUESTAGENT_ARCH_S390X=y
-CONFIG_GUESTAGENT_COMPRESS=n
+CONFIG_GUESTAGENT_COMPRESS=y
diff --git a/go.mod b/go.mod
index 55b1085d..d966f9d4 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/cheggaaa/pb/v3 v3.1.7 // gomodjail:unconfined
 	github.com/containerd/containerd v1.7.27
 	github.com/containerd/continuity v0.4.5
-	github.com/containers/gvisor-tap-vsock v0.8.5 // gomodjail:unconfined
+	github.com/containers/gvisor-tap-vsock v0.8.6 // gomodjail:unconfined
 	github.com/coreos/go-semver v0.3.1
 	github.com/cpuguy83/go-md2man/v2 v2.0.7
 	github.com/cyphar/filepath-securejoin v0.4.1
@@ -31,7 +31,7 @@ require (
 	github.com/mattn/go-isatty v0.0.20
 	github.com/mattn/go-shellwords v1.0.12
 	github.com/mdlayher/vsock v1.2.1 // gomodjail:unconfined
-	github.com/miekg/dns v1.1.65 // gomodjail:unconfined
+	github.com/miekg/dns v1.1.66 // gomodjail:unconfined
 	github.com/mikefarah/yq/v4 v4.45.2
 	github.com/nxadm/tail v1.4.11 // gomodjail:unconfined
 	github.com/opencontainers/go-digest v1.0.0
@@ -51,9 +51,9 @@ require (
 	google.golang.org/protobuf v1.36.6 // gomodjail:unconfined
 	gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
 	gotest.tools/v3 v3.5.2
-	k8s.io/api v0.32.3
-	k8s.io/apimachinery v0.32.3
-	k8s.io/client-go v0.32.3
+	k8s.io/api v0.32.4
+	k8s.io/apimachinery v0.32.4
+	k8s.io/client-go v0.32.4
 )
 
 require (
@@ -124,7 +124,7 @@ require (
 	golang.org/x/oauth2 v0.26.0 // indirect
 	golang.org/x/term v0.32.0 // indirect
 	golang.org/x/time v0.7.0 // indirect
-	golang.org/x/tools v0.30.0 // indirect
+	golang.org/x/tools v0.32.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
diff --git a/go.sum b/go.sum
index 293fe3bb..b4e6a218 100644
--- a/go.sum
+++ b/go.sum
@@ -42,8 +42,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
 github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
 github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
 github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
-github.com/containers/gvisor-tap-vsock v0.8.5 h1:s7PA8znsZ4mamev5nNLsQqduYSlz1Ze5TWjfXnAfpEs=
-github.com/containers/gvisor-tap-vsock v0.8.5/go.mod h1:A2e733OAwqSmHtWXF1WRbDZcqaZgskR5F0qk1daSz/k=
+github.com/containers/gvisor-tap-vsock v0.8.6 h1:9SeAXK+K2o36CtrgYk6zRXbU3zrayjvkrI8b7/O6u5A=
+github.com/containers/gvisor-tap-vsock v0.8.6/go.mod h1:+0mtKmm4STeSDnZe+DGnIwN4EH2f7AcWir7PwT28Ti0=
 github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
 github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -192,8 +192,8 @@ github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnE
 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
-github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
-github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
+github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
+github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
 github.com/mikefarah/yq/v4 v4.45.2 h1:onRcALNxz9RCUbn8EVj1qr0cFu/wjiPEzWy39F6fdNM=
 github.com/mikefarah/yq/v4 v4.45.2/go.mod h1:f68Ictsc8M1ufNdClEpewntHU+3KKbnUwom4szZ7JwE=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -211,8 +211,8 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
 github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
-github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
-github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
+github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
+github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
@@ -408,8 +408,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
-golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
+golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
+golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -438,12 +438,12 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
 gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f h1:O2w2DymsOlM/nv2pLNWCMCYOldgBBMkD7H0/prN5W2k=
 gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
-k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
-k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
-k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
-k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
-k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
+k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4=
+k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g=
+k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I=
+k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.4 h1:zaGJS7xoYOYumoWIFXlcVrsiYioRPrXGO7dBfVC5R6M=
+k8s.io/client-go v0.32.4/go.mod h1:k0jftcyYnEtwlFW92xC7MTtFv5BNcZBr+zn9jPlT9Ic=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
diff --git a/hack/validate-artifact.sh b/hack/validate-artifact.sh
new file mode 100755
index 00000000..00a51285
--- /dev/null
+++ b/hack/validate-artifact.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+#
+# This script validates that lima-<VERSION>-Darwin-arm64.tar.gz
+# contains lima-guestagent.Linux-aarch64
+# but does not contain share/lima/lima-guestagent.Linux-x86_64
+
+set -eu -o pipefail
+
+must_contain() {
+	tmp="$(mktemp)"
+	tar tzf "$1" >"$tmp"
+	if ! grep -q "$2" "$tmp"; then
+		echo >&2 "ERROR: $1 must contain $2"
+		cat "$tmp"
+		rm -f "$tmp"
+		exit 1
+	fi
+	rm -f "$tmp"
+}
+
+must_not_contain() {
+	tmp="$(mktemp)"
+	tar tzf "$1" >"$tmp"
+	if grep -q "$2" "$tmp"; then
+		echo >&2 "ERROR: $1 must not contain $2"
+		cat "$tmp"
+		rm -f "$tmp"
+		exit 1
+	fi
+	rm -f "$tmp"
+}
+
+validate_artifact() {
+	FILE="$1"
+	MYARCH="x86_64"
+	OTHERARCH="aarch64"
+	if [[ $FILE == *"aarch64"* || $FILE == *"arm64"* ]]; then
+		MYARCH="aarch64"
+		OTHERARCH="x86_64"
+	fi
+	if [[ $FILE == *"go-mod-vendor.tar.gz" ]]; then
+		: NOP
+	elif [[ $FILE == *"lima-additional-guestagents"*".tar.gz" ]]; then
+		must_not_contain "$FILE" "lima-guestagent.Linux-$MYARCH"
+		must_contain "$FILE" "lima-guestagent.Linux-$OTHERARCH"
+	elif [[ $FILE == *"lima-"*".tar.gz" ]]; then
+		must_not_contain "$FILE" "lima-guestagent.Linux-$OTHERARCH"
+		must_contain "$FILE" "lima-guestagent.Linux-$MYARCH"
+	else
+		echo >&2 "ERROR: Unexpected file: $FILE"
+		exit 1
+	fi
+}
+
+for FILE in "$@"; do
+	validate_artifact "$FILE"
+done
diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go
index cab834b7..a0e4370f 100644
--- a/pkg/cidata/cidata.go
+++ b/pkg/cidata/cidata.go
@@ -30,7 +30,6 @@ import (
 	"github.com/lima-vm/lima/pkg/osutil"
 	"github.com/lima-vm/lima/pkg/sshutil"
 	"github.com/lima-vm/lima/pkg/store/filenames"
-	"github.com/lima-vm/lima/pkg/usrlocalsharelima"
 	"github.com/sirupsen/logrus"
 )
 
@@ -355,7 +354,7 @@ func GenerateCloudConfig(instDir, name string, instConfig *limayaml.LimaYAML) er
 	return os.WriteFile(filepath.Join(instDir, filenames.CloudConfig), config, 0o444)
 }
 
-func GenerateISO9660(instDir, name string, instConfig *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, nerdctlArchive string, vsockPort int, virtioPort string) error {
+func GenerateISO9660(instDir, name string, instConfig *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, guestAgentBinary, nerdctlArchive string, vsockPort int, virtioPort string) error {
 	args, err := templateArgs(true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort)
 	if err != nil {
 		return err
@@ -391,31 +390,32 @@ func GenerateISO9660(instDir, name string, instConfig *limayaml.LimaYAML, udpDNS
 		}
 	}
 
-	guestAgentBinary, err := usrlocalsharelima.GuestAgentBinary(*instConfig.OS, *instConfig.Arch)
-	if err != nil {
-		return err
-	}
-	var guestAgent io.ReadCloser
-	guestAgent, err = os.Open(guestAgentBinary)
-	if err != nil {
-		if !errors.Is(err, os.ErrNotExist) {
-			return err
-		}
-		compressedGuestAgent, err := os.Open(guestAgentBinary + ".gz")
-		if err != nil {
-			return err
-		}
-		logrus.Debugf("Decompressing %s.gz", guestAgentBinary)
-		guestAgent, err = gzip.NewReader(compressedGuestAgent)
-		if err != nil {
-			return err
+	if guestAgentBinary != "" {
+		var guestAgent io.ReadCloser
+		if strings.HasSuffix(guestAgentBinary, ".gz") {
+			logrus.Debugf("Decompressing %s", guestAgentBinary)
+			guestAgentGz, err := os.Open(guestAgentBinary)
+			if err != nil {
+				return err
+			}
+			defer guestAgentGz.Close()
+			guestAgent, err = gzip.NewReader(guestAgentGz)
+			if err != nil {
+				return err
+			}
+		} else {
+			guestAgent, err = os.Open(guestAgentBinary)
+			if err != nil {
+				return err
+			}
 		}
+
+		defer guestAgent.Close()
+		layout = append(layout, iso9660util.Entry{
+			Path:   "lima-guestagent",
+			Reader: guestAgent,
+		})
 	}
-	defer guestAgent.Close()
-	layout = append(layout, iso9660util.Entry{
-		Path:   "lima-guestagent",
-		Reader: guestAgent,
-	})
 
 	if nerdctlArchive != "" {
 		nftgz := args.Containerd.Archive
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index f48f8dd8..36e6d243 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -75,11 +75,19 @@ type HostAgent struct {
 }
 
 type options struct {
-	nerdctlArchive string // local path, not URL
+	guestAgentBinary string
+	nerdctlArchive   string // local path, not URL
 }
 
 type Opt func(*options) error
 
+func WithGuestAgentBinary(s string) Opt {
+	return func(o *options) error {
+		o.guestAgentBinary = s
+		return nil
+	}
+}
+
 func WithNerdctlArchive(s string) Opt {
 	return func(o *options) error {
 		o.nerdctlArchive = s
@@ -134,7 +142,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
 	if err := cidata.GenerateCloudConfig(inst.Dir, instName, inst.Config); err != nil {
 		return nil, err
 	}
-	if err := cidata.GenerateISO9660(inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive, vSockPort, virtioPort); err != nil {
+	if err := cidata.GenerateISO9660(inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort); err != nil {
 		return nil, err
 	}
 
diff --git a/pkg/infoutil/infoutil.go b/pkg/infoutil/infoutil.go
index c3c2469b..82daac81 100644
--- a/pkg/infoutil/infoutil.go
+++ b/pkg/infoutil/infoutil.go
@@ -4,19 +4,29 @@
 package infoutil
 
 import (
+	"errors"
+	"io/fs"
+
 	"github.com/lima-vm/lima/pkg/driverutil"
 	"github.com/lima-vm/lima/pkg/limayaml"
 	"github.com/lima-vm/lima/pkg/store/dirnames"
 	"github.com/lima-vm/lima/pkg/templatestore"
+	"github.com/lima-vm/lima/pkg/usrlocalsharelima"
 	"github.com/lima-vm/lima/pkg/version"
+	"github.com/sirupsen/logrus"
 )
 
 type Info struct {
-	Version         string                   `json:"version"`
-	Templates       []templatestore.Template `json:"templates"`
-	DefaultTemplate *limayaml.LimaYAML       `json:"defaultTemplate"`
-	LimaHome        string                   `json:"limaHome"`
-	VMTypes         []string                 `json:"vmTypes"` // since Lima v0.14.2
+	Version         string                       `json:"version"`
+	Templates       []templatestore.Template     `json:"templates"`
+	DefaultTemplate *limayaml.LimaYAML           `json:"defaultTemplate"`
+	LimaHome        string                       `json:"limaHome"`
+	VMTypes         []string                     `json:"vmTypes"`     // since Lima v0.14.2
+	GuestAgents     map[limayaml.Arch]GuestAgent `json:"guestAgents"` // since Lima v1.1.0
+}
+
+type GuestAgent struct {
+	Location string `json:"location"` // since Lima v1.1.0
 }
 
 func GetInfo() (*Info, error) {
@@ -32,6 +42,7 @@ func GetInfo() (*Info, error) {
 		Version:         version.Version,
 		DefaultTemplate: y,
 		VMTypes:         driverutil.Drivers(),
+		GuestAgents:     make(map[limayaml.Arch]GuestAgent),
 	}
 	info.Templates, err = templatestore.Templates()
 	if err != nil {
@@ -41,5 +52,19 @@ func GetInfo() (*Info, error) {
 	if err != nil {
 		return nil, err
 	}
+	for _, arch := range limayaml.ArchTypes {
+		bin, err := usrlocalsharelima.GuestAgentBinary(limayaml.LINUX, arch)
+		if err != nil {
+			if errors.Is(err, fs.ErrNotExist) {
+				logrus.WithError(err).Debugf("Failed to resolve the guest agent binary for %q", arch)
+			} else {
+				logrus.WithError(err).Warnf("Failed to resolve the guest agent binary for %q", arch)
+			}
+			continue
+		}
+		info.GuestAgents[arch] = GuestAgent{
+			Location: bin,
+		}
+	}
 	return info, nil
 }
diff --git a/pkg/instance/start.go b/pkg/instance/start.go
index c76a6957..9144d9af 100644
--- a/pkg/instance/start.go
+++ b/pkg/instance/start.go
@@ -16,10 +16,15 @@ import (
 	"text/template"
 	"time"
 
+	"github.com/docker/go-units"
+	"github.com/lima-vm/go-qcow2reader"
 	"github.com/lima-vm/lima/pkg/driver"
 	"github.com/lima-vm/lima/pkg/driverutil"
 	"github.com/lima-vm/lima/pkg/executil"
+	"github.com/lima-vm/lima/pkg/nativeimgutil"
 	"github.com/lima-vm/lima/pkg/osutil"
+	"github.com/lima-vm/lima/pkg/qemu/imgutil"
+	"github.com/lima-vm/lima/pkg/usrlocalsharelima"
 	"github.com/mattn/go-isatty"
 
 	"github.com/lima-vm/lima/pkg/downloader"
@@ -72,11 +77,20 @@ func ensureNerdctlArchiveCache(ctx context.Context, y *limayaml.LimaYAML, create
 
 type Prepared struct {
 	Driver              driver.Driver
+	GuestAgent          string
 	NerdctlArchiveCache string
 }
 
 // Prepare ensures the disk, the nerdctl archive, etc.
 func Prepare(ctx context.Context, inst *store.Instance) (*Prepared, error) {
+	var guestAgent string
+	if !*inst.Config.Plain {
+		var err error
+		guestAgent, err = usrlocalsharelima.GuestAgentBinary(*inst.Config.OS, *inst.Config.Arch)
+		if err != nil {
+			return nil, err
+		}
+	}
 	limaDriver := driverutil.CreateTargetDriverInstance(&driver.BaseDriver{
 		Instance: inst,
 	})
@@ -97,6 +111,12 @@ func Prepare(ctx context.Context, inst *store.Instance) (*Prepared, error) {
 	if err := limaDriver.CreateDisk(ctx); err != nil {
 		return nil, err
 	}
+
+	// Ensure diffDisk size matches the store
+	if err := prepareDiffDisk(inst); err != nil {
+		return nil, err
+	}
+
 	nerdctlArchiveCache, err := ensureNerdctlArchiveCache(ctx, inst.Config, created)
 	if err != nil {
 		return nil, err
@@ -104,6 +124,7 @@ func Prepare(ctx context.Context, inst *store.Instance) (*Prepared, error) {
 
 	return &Prepared{
 		Driver:              limaDriver,
+		GuestAgent:          guestAgent,
 		NerdctlArchiveCache: nerdctlArchiveCache,
 	}, nil
 }
@@ -171,6 +192,9 @@ func Start(ctx context.Context, inst *store.Instance, limactl string, launchHost
 	if prepared.Driver.CanRunGUI() {
 		args = append(args, "--run-gui")
 	}
+	if prepared.GuestAgent != "" {
+		args = append(args, "--guestagent", prepared.GuestAgent)
+	}
 	if prepared.NerdctlArchiveCache != "" {
 		args = append(args, "--nerdctl-archive", prepared.NerdctlArchiveCache)
 	}
@@ -363,3 +387,55 @@ func ShowMessage(inst *store.Instance) error {
 	}
 	return scanner.Err()
 }
+
+// prepareDiffDisk checks the disk size difference between inst.Disk and yaml.Disk.
+// If there is no diffDisk, return nil (the instance has not been initialized or started yet).
+func prepareDiffDisk(inst *store.Instance) error {
+	diffDisk := filepath.Join(inst.Dir, filenames.DiffDisk)
+
+	// Handle the instance initialization
+	_, err := os.Stat(diffDisk)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil
+		}
+		return err
+	}
+
+	f, err := os.Open(diffDisk)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	img, err := qcow2reader.Open(f)
+	if err != nil {
+		return err
+	}
+
+	diskSize := img.Size()
+	format := string(img.Type())
+
+	if inst.Disk == diskSize {
+		return nil
+	}
+
+	logrus.Infof("Resize instance %s's disk from %s to %s", inst.Name, units.BytesSize(float64(diskSize)), units.BytesSize(float64(inst.Disk)))
+
+	if inst.Disk < diskSize {
+		inst.Disk = diskSize
+		return errors.New("diffDisk: Shrinking is currently unavailable")
+	}
+
+	if format == "raw" {
+		err = nativeimgutil.ResizeRawDisk(diffDisk, int(inst.Disk))
+	} else {
+		err = imgutil.ResizeDisk(diffDisk, format, int(inst.Disk))
+	}
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/jsonschemautil/jsonschemautil.go b/pkg/jsonschemautil/jsonschemautil.go
new file mode 100644
index 00000000..7f7b16a0
--- /dev/null
+++ b/pkg/jsonschemautil/jsonschemautil.go
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package jsonschemautil
+
+import (
+	"os"
+
+	"github.com/goccy/go-yaml"
+	"github.com/santhosh-tekuri/jsonschema/v6"
+)
+
+func Validate(schemafile, instancefile string) error {
+	compiler := jsonschema.NewCompiler()
+	schema, err := compiler.Compile(schemafile)
+	if err != nil {
+		return err
+	}
+	instance, err := os.ReadFile(instancefile)
+	if err != nil {
+		return err
+	}
+	var y any
+	err = yaml.Unmarshal(instance, &y)
+	if err != nil {
+		return err
+	}
+	return schema.Validate(y)
+}
diff --git a/pkg/limayaml/containerd.yaml b/pkg/limayaml/containerd.yaml
index 3c65f720..3d77de58 100644
--- a/pkg/limayaml/containerd.yaml
+++ b/pkg/limayaml/containerd.yaml
@@ -1,9 +1,9 @@
 archives:
-- location: https://github.com/containerd/nerdctl/releases/download/v2.0.5/nerdctl-full-2.0.5-linux-amd64.tar.gz
+- location: https://github.com/containerd/nerdctl/releases/download/v2.1.1/nerdctl-full-2.1.1-linux-amd64.tar.gz
   arch: x86_64
-  digest: sha256:0ce510e7ea837fd9619664ec50dc4becfd62b4bc0cb54fe00e773b0e1bb5f1bf
-- location: https://github.com/containerd/nerdctl/releases/download/v2.0.5/nerdctl-full-2.0.5-linux-arm64.tar.gz
+  digest: sha256:f4fcb3ba57dabf3f1a07d6f58e1917c3b8a5ef7a84fa268c0f706f214e78b48f
+- location: https://github.com/containerd/nerdctl/releases/download/v2.1.1/nerdctl-full-2.1.1-linux-arm64.tar.gz
   arch: aarch64
-  digest: sha256:865055935744a24e733501eb6d41fd5123880166521c14d409ceb91180b93802
+  digest: sha256:a5004520d47b2823dd07888378c70e1085f4f14bcb5f575ea8426d5b5b37c7c6
 # No arm-v7
 # No riscv64
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index 4082b82d..d72e5b60 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -10,13 +10,13 @@ import (
 )
 
 type LimaYAML struct {
-	Base                  BaseTemplates `yaml:"base,omitempty" json:"base,omitempty" jsonschema:"nullable"`
+	Base                  BaseTemplates `yaml:"base,omitempty" json:"base,omitempty"`
 	MinimumLimaVersion    *string       `yaml:"minimumLimaVersion,omitempty" json:"minimumLimaVersion,omitempty" jsonschema:"nullable"`
 	VMType                *VMType       `yaml:"vmType,omitempty" json:"vmType,omitempty" jsonschema:"nullable"`
 	VMOpts                VMOpts        `yaml:"vmOpts,omitempty" json:"vmOpts,omitempty"`
 	OS                    *OS           `yaml:"os,omitempty" json:"os,omitempty" jsonschema:"nullable"`
 	Arch                  *Arch         `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"nullable"`
-	Images                []Image       `yaml:"images" json:"images"` // REQUIRED
+	Images                []Image       `yaml:"images,omitempty" json:"images,omitempty" jsonschema:"nullable"`
 	CPUType               CPUType       `yaml:"cpuType,omitempty" json:"cpuType,omitempty" jsonschema:"nullable"`
 	CPUs                  *int          `yaml:"cpus,omitempty" json:"cpus,omitempty" jsonschema:"nullable"`
 	Memory                *string       `yaml:"memory,omitempty" json:"memory,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes
@@ -58,7 +58,7 @@ type BaseTemplates []LocatorWithDigest
 
 type LocatorWithDigest struct {
 	URL    string  `yaml:"url" json:"url"`
-	Digest *string `yaml:"digest,omitempty" json:"digest,omitempty" jsonschema:"nullable"` // TODO currently unused
+	Digest *string `yaml:"digest,omitempty" json:"digest,omitempty"` // TODO currently unused
 }
 
 type (
@@ -229,7 +229,7 @@ const (
 type Provision struct {
 	Mode                            ProvisionMode      `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=system"`
 	SkipDefaultDependencyResolution *bool              `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"`
-	Script                          string             `yaml:"script" json:"script"`
+	Script                          string             `yaml:"script,omitempty" json:"script,omitempty"`
 	File                            *LocatorWithDigest `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
 	Playbook                        string             `yaml:"playbook,omitempty" json:"playbook,omitempty"` // DEPRECATED
 	// All ProvisionData fields must be nil unless Mode is ProvisionModeData
diff --git a/pkg/limayaml/marshal.go b/pkg/limayaml/marshal.go
index 0380fa9b..70caee2f 100644
--- a/pkg/limayaml/marshal.go
+++ b/pkg/limayaml/marshal.go
@@ -45,11 +45,6 @@ func unmarshalBaseTemplates(dst *BaseTemplates, b []byte) error {
 		*dst = BaseTemplates{LocatorWithDigest{URL: s}}
 		return nil
 	}
-	var locator LocatorWithDigest
-	if err := yaml.Unmarshal(b, &locator); err == nil {
-		*dst = BaseTemplates{locator}
-		return nil
-	}
 	return yaml.UnmarshalWithOptions(b, dst, yaml.CustomUnmarshaler[LocatorWithDigest](unmarshalLocatorWithDigest))
 }
 
diff --git a/pkg/nativeimgutil/nativeimgutil.go b/pkg/nativeimgutil/nativeimgutil.go
index 64308000..52dcf2eb 100644
--- a/pkg/nativeimgutil/nativeimgutil.go
+++ b/pkg/nativeimgutil/nativeimgutil.go
@@ -19,7 +19,6 @@ import (
 	"github.com/lima-vm/go-qcow2reader/image/qcow2"
 	"github.com/lima-vm/go-qcow2reader/image/raw"
 	"github.com/lima-vm/lima/pkg/progressbar"
-	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/sirupsen/logrus"
 )
 
@@ -34,13 +33,12 @@ func RoundUp(size int) int {
 	return sectors * sectorSize
 }
 
-// CreateRawDataDisk creates an empty raw data disk.
-func CreateRawDataDisk(dir string, size int) error {
-	dataDisk := filepath.Join(dir, filenames.DataDisk)
-	if _, err := os.Stat(dataDisk); err == nil || !errors.Is(err, fs.ErrNotExist) {
+// CreateRawDisk creates an empty raw data disk.
+func CreateRawDisk(disk string, size int) error {
+	if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
 		return err
 	}
-	f, err := os.Create(dataDisk)
+	f, err := os.Create(disk)
 	if err != nil {
 		return err
 	}
@@ -49,11 +47,10 @@ func CreateRawDataDisk(dir string, size int) error {
 	return f.Truncate(int64(roundedSize))
 }
 
-// ResizeRawDataDisk resizes a raw data disk.
-func ResizeRawDataDisk(dir string, size int) error {
-	dataDisk := filepath.Join(dir, filenames.DataDisk)
+// ResizeRawDisk resizes a raw data disk.
+func ResizeRawDisk(disk string, size int) error {
 	roundedSize := RoundUp(size)
-	return os.Truncate(dataDisk, int64(roundedSize))
+	return os.Truncate(disk, int64(roundedSize))
 }
 
 // ConvertToRaw converts a source disk into a raw disk.
diff --git a/pkg/qemu/imgutil/imgutil.go b/pkg/qemu/imgutil/imgutil.go
index 2fffbfd1..18512bf0 100644
--- a/pkg/qemu/imgutil/imgutil.go
+++ b/pkg/qemu/imgutil/imgutil.go
@@ -6,8 +6,12 @@ package imgutil
 import (
 	"bytes"
 	"encoding/json"
+	"errors"
 	"fmt"
+	"io/fs"
+	"os"
 	"os/exec"
+	"strconv"
 
 	"github.com/sirupsen/logrus"
 )
@@ -22,6 +26,29 @@ type InfoFormatSpecific struct {
 	Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7
 }
 
+func CreateDisk(disk, format string, size int) error {
+	if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
+		// disk already exists
+		return err
+	}
+
+	args := []string{"create", "-f", format, disk, strconv.Itoa(size)}
+	cmd := exec.Command("qemu-img", args...)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
+	}
+	return nil
+}
+
+func ResizeDisk(disk, format string, size int) error {
+	args := []string{"resize", "-f", format, disk, strconv.Itoa(size)}
+	cmd := exec.Command("qemu-img", args...)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
+	}
+	return nil
+}
+
 func (sp *InfoFormatSpecific) Qcow2() *InfoFormatSpecificDataQcow2 {
 	if sp.Type != "qcow2" {
 		return nil
diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go
index 4211d2e6..12e148bf 100644
--- a/pkg/qemu/qemu.go
+++ b/pkg/qemu/qemu.go
@@ -47,10 +47,42 @@ type Config struct {
 	VirtioGA     bool
 }
 
-// MinimumQemuVersion is the minimum supported QEMU version.
-const (
-	MinimumQemuVersion = "4.0.0"
-)
+// minimumQemuVersion returns hardMin and softMin.
+//
+// hardMin is the hard minimum version of QEMU.
+// The driver immediately returns the error when QEMU is older than this version.
+//
+// softMin is the oldest recommended version of QEMU.
+// softMin must be >= hardMin.
+//
+// When updating this function, make sure to update
+// `website/content/en/docs/config/vmtype.md` too.
+func minimumQemuVersion() (hardMin, softMin semver.Version) {
+	var h, s string
+	switch runtime.GOOS {
+	case "darwin":
+		switch runtime.GOARCH {
+		case "arm64":
+			// https://gitlab.com/qemu-project/qemu/-/issues/1990
+			h, s = "8.2.1", "8.2.1"
+		default:
+			// The code specific to QEMU < 7.0 on macOS (https://github.com/lima-vm/lima/pull/703)
+			// was removed in https://github.com/lima-vm/lima/pull/3491
+			h, s = "7.0.0", "8.2.1"
+		}
+	default:
+		// hardMin: Untested and maybe does not even work.
+		// softMin: Ubuntu 22.04's QEMU. The oldest version that can be easily tested on GitHub Actions.
+		h, s = "4.0.0", "6.2.0"
+	}
+	hardMin, softMin = *semver.New(h), *semver.New(s)
+	if softMin.LessThan(hardMin) {
+		// NOTREACHED
+		logrus.Fatalf("internal error: QEMU: soft minimum version %v must be >= hard minimum version %v",
+			softMin, hardMin)
+	}
+	return hardMin, softMin
+}
 
 // EnsureDisk also ensures the kernel and the initrd.
 func EnsureDisk(ctx context.Context, cfg Config) error {
@@ -127,32 +159,6 @@ func EnsureDisk(ctx context.Context, cfg Config) error {
 	return nil
 }
 
-func CreateDataDisk(dir, format string, size int) error {
-	dataDisk := filepath.Join(dir, filenames.DataDisk)
-	if _, err := os.Stat(dataDisk); err == nil || !errors.Is(err, fs.ErrNotExist) {
-		// datadisk already exists
-		return err
-	}
-
-	args := []string{"create", "-f", format, dataDisk, strconv.Itoa(size)}
-	cmd := exec.Command("qemu-img", args...)
-	if out, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
-	}
-	return nil
-}
-
-func ResizeDataDisk(dir, format string, size int) error {
-	dataDisk := filepath.Join(dir, filenames.DataDisk)
-
-	args := []string{"resize", "-f", format, dataDisk, strconv.Itoa(size)}
-	cmd := exec.Command("qemu-img", args...)
-	if out, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
-	}
-	return nil
-}
-
 func newQmpClient(cfg Config) (*qmp.SocketMonitor, error) {
 	qmpSock := filepath.Join(cfg.InstanceDir, filenames.QMPSock)
 	qmpClient, err := qmp.NewSocketMonitor("unix", qmpSock, 5*time.Second)
@@ -313,9 +319,6 @@ type features struct {
 	// e.g. "Available CPUs:\n...\nx86 base...\nx86 host...\n...\n"
 	// Not machine-readable, but checking strings.Contains() should be fine.
 	CPUHelp []byte
-
-	// VersionGEQ7 is true when the QEMU version seems v7.0.0 or later
-	VersionGEQ7 bool
 }
 
 func inspectFeatures(exe, machine string) (*features, error) {
@@ -359,7 +362,6 @@ func inspectFeatures(exe, machine string) (*features, error) {
 			f.MachineHelp = stderr.Bytes()
 		}
 	}
-	f.VersionGEQ7 = strings.Contains(string(f.MachineHelp), "-7.0")
 
 	// Avoid error: "No machine specified, and there is no default"
 	cmd = exec.Command(exe, "-cpu", "help", "-machine", machine)
@@ -377,56 +379,15 @@ func inspectFeatures(exe, machine string) (*features, error) {
 	return &f, nil
 }
 
-// showDarwinARM64HVFQEMU620Warning shows a warning on M1 macOS when QEMU is older than 6.2.0_1.
-//
-// See:
-// - https://gitlab.com/qemu-project/qemu/-/issues/899
-// - https://github.com/Homebrew/homebrew-core/pull/96743
-// - https://github.com/lima-vm/lima/issues/712
-func showDarwinARM64HVFQEMU620Warning(exe, accel string, features *features) {
-	if runtime.GOOS != "darwin" {
-		return
-	}
-	if runtime.GOARCH != "arm64" {
-		return
-	}
-	if accel != "hvf" {
-		return
-	}
-	if features.VersionGEQ7 {
-		return
-	}
-	if exeFull, err := exec.LookPath(exe); err == nil {
-		if exeResolved, err2 := filepath.EvalSymlinks(exeFull); err2 == nil {
-			if strings.Contains(exeResolved, "Cellar/qemu/6.2.0_") {
-				// Homebrew's QEMU 6.2.0_1 or later
-				return
-			}
-		}
-	}
-	w := "This version of QEMU might not be able to boot recent Linux guests on M1 macOS hosts."
-	if _, err := exec.LookPath("brew"); err == nil {
-		w += "Run `brew upgrade` and make sure your QEMU version is 6.2.0_1 or later."
-	} else {
-		w += `Reinstall QEMU with the following commits (included in QEMU 7.0.0):
-- https://github.com/qemu/qemu/commit/ad99f64f "hvf: arm: Use macros for sysreg shift/masking"
-- https://github.com/qemu/qemu/commit/7f6c295c "hvf: arm: Handle unknown ID registers as RES0"
-`
-		w += "See https://github.com/Homebrew/homebrew-core/pull/96743 for the further information."
-	}
-	logrus.Warn(w)
-}
-
 // adjustMemBytesDarwinARM64HVF adjusts the memory to be <= 3 GiB, only when the following conditions are met:
 //
 // - Host OS   <  macOS 12.4
 // - Host Arch == arm64
 // - Accel     == hvf
-// - QEMU      >= 7.0
 //
 // This adjustment is required for avoiding host kernel panic. The issue was fixed in macOS 12.4 Beta 1.
 // See https://github.com/lima-vm/lima/issues/795 https://gitlab.com/qemu-project/qemu/-/issues/903#note_911000975
-func adjustMemBytesDarwinARM64HVF(memBytes int64, accel string, features *features) int64 {
+func adjustMemBytesDarwinARM64HVF(memBytes int64, accel string) int64 {
 	const safeSize = 3 * 1024 * 1024 * 1024 // 3 GiB
 	if memBytes <= safeSize {
 		return memBytes
@@ -440,9 +401,6 @@ func adjustMemBytesDarwinARM64HVF(memBytes int64, accel string, features *featur
 	if accel != "hvf" {
 		return memBytes
 	}
-	if !features.VersionGEQ7 {
-		return memBytes
-	}
 	macOSProductVersion, err := osutil.ProductVersion()
 	if err != nil {
 		logrus.Warn(err)
@@ -451,8 +409,8 @@ func adjustMemBytesDarwinARM64HVF(memBytes int64, accel string, features *featur
 	if !macOSProductVersion.LessThan(*semver.New("12.4.0")) {
 		return memBytes
 	}
-	logrus.Warnf("Reducing the guest memory from %s to %s, to avoid host kernel panic on macOS <= 12.3 with QEMU >= 7.0; "+
-		"Please update macOS to 12.4 or later, or downgrade QEMU to 6.2; "+
+	logrus.Warnf("Reducing the guest memory from %s to %s, to avoid host kernel panic on macOS <= 12.3; "+
+		"Please update macOS to 12.4 or later; "+
 		"See https://github.com/lima-vm/lima/issues/795 for the further background.",
 		units.BytesSize(float64(memBytes)), units.BytesSize(float64(safeSize)))
 	memBytes = safeSize
@@ -497,16 +455,16 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 		logrus.WithError(err).Warning("Failed to detect QEMU version")
 	} else {
 		logrus.Debugf("QEMU version %s detected", version.String())
-		if version.LessThan(*semver.New(MinimumQemuVersion)) {
-			logrus.Fatalf("QEMU %v is too old, %v or later required", version, MinimumQemuVersion)
+		hardMin, softMin := minimumQemuVersion()
+		if version.LessThan(hardMin) {
+			logrus.Fatalf("QEMU %v is too old, %v or later required", version, hardMin)
+		}
+		if version.LessThan(softMin) {
+			logrus.Warnf("QEMU %v is too old, %v or later is recommended", version, softMin)
 		}
 		if y.VMOpts.QEMU.MinimumVersion != nil && version.LessThan(*semver.New(*y.VMOpts.QEMU.MinimumVersion)) {
 			logrus.Fatalf("QEMU %v is too old, template requires %q or later", version, *y.VMOpts.QEMU.MinimumVersion)
 		}
-		if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && version.Equal(*semver.New("8.2.0")) {
-			logrus.Fatal("QEMU 8.2.0 is no longer supported on ARM Mac due to <https://gitlab.com/qemu-project/qemu/-/issues/1990>. " +
-				"Please upgrade QEMU to v8.2.1 (or downgrade to v8.1.x).")
-		}
 	}
 
 	// Architecture
@@ -514,14 +472,13 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 	if !strings.Contains(string(features.AccelHelp), accel) {
 		return "", nil, fmt.Errorf("accelerator %q is not supported by %s", accel, exe)
 	}
-	showDarwinARM64HVFQEMU620Warning(exe, accel, features)
 
 	// Memory
 	memBytes, err := units.RAMInBytes(*y.Memory)
 	if err != nil {
 		return "", nil, err
 	}
-	memBytes = adjustMemBytesDarwinARM64HVF(memBytes, accel, features)
+	memBytes = adjustMemBytesDarwinARM64HVF(memBytes, accel)
 	args = appendArgsIfNoConflict(args, "-m", strconv.Itoa(int(memBytes>>20)))
 
 	if *y.MountType == limayaml.VIRTIOFS {
@@ -569,14 +526,6 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 		}
 	case limayaml.AARCH64:
 		machine := "virt,accel=" + accel
-		// QEMU >= 7.0 requires highmem=off NOT to be set, otherwise fails with "Addressing limited to 32 bits, but memory exceeds it by 1073741824 bytes"
-		// QEMU <  7.0 requires highmem=off to be set, otherwise fails with "VCPU supports less PA bits (36) than requested by the memory map (40)"
-		// https://github.com/lima-vm/lima/issues/680
-		// https://github.com/lima-vm/lima/pull/24
-		// But when the memory size is <= 3 GiB, we can always set highmem=off.
-		if !features.VersionGEQ7 || memBytes <= 3*1024*1024*1024 {
-			machine += ",highmem=off"
-		}
 		args = appendArgsIfNoConflict(args, "-machine", machine)
 	case limayaml.RISCV64:
 		// https://github.com/tianocore/edk2/blob/edk2-stable202408/OvmfPkg/RiscVVirt/README.md#test
@@ -876,23 +825,15 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 	}
 
 	switch *y.Arch {
+	// FIXME: use virtio-gpu on all the architectures
 	case limayaml.X8664, limayaml.RISCV64:
 		args = append(args, "-device", "virtio-vga")
-		args = append(args, "-device", "virtio-keyboard-pci")
-		args = append(args, "-device", "virtio-"+input+"-pci")
-		args = append(args, "-device", "qemu-xhci,id=usb-bus")
-	case limayaml.AARCH64, limayaml.ARMV7L, limayaml.PPC64LE, limayaml.S390X:
-		if features.VersionGEQ7 {
-			args = append(args, "-device", "virtio-gpu")
-			args = append(args, "-device", "virtio-keyboard-pci")
-			args = append(args, "-device", "virtio-"+input+"-pci")
-		} else { // kernel panic with virtio and old versions of QEMU
-			args = append(args, "-vga", "none", "-device", "ramfb")
-			args = append(args, "-device", "usb-kbd,bus=usb-bus")
-			args = append(args, "-device", "usb-"+input+",bus=usb-bus")
-		}
-		args = append(args, "-device", "qemu-xhci,id=usb-bus")
+	default:
+		args = append(args, "-device", "virtio-gpu")
 	}
+	args = append(args, "-device", "virtio-keyboard-pci")
+	args = append(args, "-device", "virtio-"+input+"-pci")
+	args = append(args, "-device", "qemu-xhci,id=usb-bus")
 
 	// Parallel
 	args = append(args, "-parallel", "none")
diff --git a/pkg/store/disk.go b/pkg/store/disk.go
index c4d0f2a9..a9bc102c 100644
--- a/pkg/store/disk.go
+++ b/pkg/store/disk.go
@@ -11,7 +11,6 @@ import (
 	"path/filepath"
 
 	"github.com/lima-vm/go-qcow2reader"
-	"github.com/lima-vm/lima/pkg/qemu/imgutil"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 )
 
@@ -61,35 +60,25 @@ func InspectDisk(diskName string) (*Disk, error) {
 	return disk, nil
 }
 
-// inspectDisk attempts to inspect the disk size and format by itself,
-// and falls back to inspectDiskWithQemuImg on an error.
+// inspectDisk attempts to inspect the disk size and format with qcow2reader.
 func inspectDisk(fName string) (size int64, format string, _ error) {
 	f, err := os.Open(fName)
 	if err != nil {
-		return inspectDiskWithQemuImg(fName)
+		return -1, "", err
 	}
 	defer f.Close()
 	img, err := qcow2reader.Open(f)
 	if err != nil {
-		return inspectDiskWithQemuImg(fName)
+		return -1, "", err
 	}
 	sz := img.Size()
 	if sz < 0 {
-		return inspectDiskWithQemuImg(fName)
+		return -1, "", fmt.Errorf("cannot determine size of %q", fName)
 	}
 
 	return sz, string(img.Type()), nil
 }
 
-// inspectDiskSizeWithQemuImg invokes `qemu-img` binary to inspect the disk size and format.
-func inspectDiskWithQemuImg(fName string) (size int64, format string, _ error) {
-	info, err := imgutil.GetInfo(fName)
-	if err != nil {
-		return -1, "", err
-	}
-	return info.VSize, info.Format, nil
-}
-
 func (d *Disk) Lock(instanceDir string) error {
 	inUseBy := filepath.Join(d.Dir, filenames.InUseBy)
 	return os.Symlink(instanceDir, inUseBy)
diff --git a/pkg/usrlocalsharelima/usrlocalsharelima.go b/pkg/usrlocalsharelima/usrlocalsharelima.go
index fcaca4ad..c96a698e 100644
--- a/pkg/usrlocalsharelima/usrlocalsharelima.go
+++ b/pkg/usrlocalsharelima/usrlocalsharelima.go
@@ -80,6 +80,7 @@ func Dir() (string, error) {
 		ostype, arch, self, gaCandidates)
 }
 
+// GuestAgentBinary returns the guest agent binary, possibly with ".gz" suffix.
 func GuestAgentBinary(ostype limayaml.OS, arch limayaml.Arch) (string, error) {
 	if ostype == "" {
 		return "", errors.New("os must be set")
@@ -91,5 +92,35 @@ func GuestAgentBinary(ostype limayaml.OS, arch limayaml.Arch) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	return filepath.Join(dir, "lima-guestagent."+ostype+"-"+arch), nil
+	uncomp := filepath.Join(dir, "lima-guestagent."+ostype+"-"+arch)
+	comp := uncomp + ".gz"
+	res, err := chooseGABinary([]string{comp, uncomp})
+	if err != nil {
+		logrus.Debug(err)
+		return "", fmt.Errorf("guest agent binary could not be found for %s-%s (Hint: try installing `lima-additional-guestagents` package)", ostype, arch)
+	}
+	return res, nil
+}
+
+func chooseGABinary(candidates []string) (string, error) {
+	var entries []string
+	for _, f := range candidates {
+		if _, err := os.Stat(f); err != nil {
+			if !errors.Is(err, fs.ErrNotExist) {
+				logrus.WithError(err).Warnf("failed to stat %q", f)
+			}
+			continue
+		}
+		entries = append(entries, f)
+	}
+	switch len(entries) {
+	case 0:
+		return "", fmt.Errorf("%w: attempted %v", fs.ErrNotExist, candidates)
+	case 1:
+		return entries[0], nil
+	default:
+		logrus.Warnf("multiple files found, choosing %q from %v; consider removing the other ones",
+			entries[0], candidates)
+		return entries[0], nil
+	}
 }
diff --git a/templates/_images/oraclelinux-8.yaml b/templates/_images/oraclelinux-8.yaml
index 73cb67f4..294a9b7f 100644
--- a/templates/_images/oraclelinux-8.yaml
+++ b/templates/_images/oraclelinux-8.yaml
@@ -1,8 +1,7 @@
 # Oracle image license: https://www.oracle.com/downloads/licenses/oracle-linux-license.html
 # Image source: https://yum.oracle.com/oracle-linux-templates.html
 
-# NOTE: EL8-based distros are known not to work on M1 chips: https://github.com/lima-vm/lima/issues/841
-# EL9-based distros are known to work.
+# Unlike AlmaLinux 8 and Rocky Linux 8, Oracle Linux 8 is known to work on Apple M1.
 
 images:
 - location: "https://yum.oracle.com/templates/OracleLinux/OL8/u10/x86_64/OL8U10_x86_64-kvm-b258.qcow2"
diff --git a/website/content/en/docs/config/vmtype.md b/website/content/en/docs/config/vmtype.md
index 3c407a5d..ff67d591 100644
--- a/website/content/en/docs/config/vmtype.md
+++ b/website/content/en/docs/config/vmtype.md
@@ -29,6 +29,10 @@ unless the config is incompatible with VZ. (e.g., legacyBIOS or 9p is enabled)
 ## QEMU
 "qemu" option makes use of QEMU to run guest operating system. 
 
+Recommended QEMU version:
+- v8.2.1 or later (macOS)
+- v6.2.0 or later (Linux)
+
 ## VZ
 
 | ⚡ Requirement | Lima >= 0.14, macOS >= 13.0 |
diff --git a/website/content/en/docs/installation/_index.md b/website/content/en/docs/installation/_index.md
index 47bdf715..25190610 100644
--- a/website/content/en/docs/installation/_index.md
+++ b/website/content/en/docs/installation/_index.md
@@ -6,10 +6,11 @@ Supported host OS:
 - macOS (the latest version is recommended)
 - Linux
 - NetBSD (untested)
+- DragonFlyBSD (untested)
 - Windows (untested)
 
 Prerequisite:
-- QEMU 7.1 or later (Required, only if QEMU driver is used)
+- QEMU (Required, only if [QEMU]({{< ref "/docs/config/vmtype#qemu" >}}) driver is used)
 
 {{< tabpane text=true >}}
 
@@ -45,6 +46,9 @@ and extract it under `/usr/local` (or somewhere else).
 ```bash
 VERSION=$(curl -fsSL https://api.github.com/repos/lima-vm/lima/releases/latest | jq -r .tag_name)
 curl -fsSL "https://github.com/lima-vm/lima/releases/download/${VERSION}/lima-${VERSION:1}-$(uname -s)-$(uname -m).tar.gz" | tar Cxzvm /usr/local
+
+# For Lima v1.1 onward
+curl -fsSL "https://github.com/lima-vm/lima/releases/download/${VERSION}/lima-additional-guestagents-${VERSION:1}-$(uname -s)-$(uname -m).tar.gz" | tar Cxzvm /usr/local
 ```
 {{% /tab %}}
-{{< /tabpane >}}
\ No newline at end of file
+{{< /tabpane >}}
diff --git a/website/content/en/docs/installation/source.md b/website/content/en/docs/installation/source.md
index e6d193da..2c23b288 100644
--- a/website/content/en/docs/installation/source.md
+++ b/website/content/en/docs/installation/source.md
@@ -36,13 +36,21 @@ export PATH=$HOME/.local/bin:$PATH
 ## Packaging Lima for Distribution
 After building Lima from source, you may want to package it for installation on other machines:
 
+1. The package for the core component and the native guest agent:
 ```bash
+make clean native
 cd _output
-# Create a compressed archive
 tar czf lima-package.tar.gz *
 ```
 
-This package can then be transferred and installed on the target system.
+2. The package for the additional guest agents:
+```
+make clean additional-guestagents
+cd _output
+tar czf lima-additional-guestagents-package.tar.gz *
+```
+
+These packages can then be transferred and installed on the target system.
 
 ## Advanced Configuration with Kconfig Tools
 (This step is not needed for most users)
diff --git a/website/content/en/docs/talks/_index.md b/website/content/en/docs/talks/_index.md
index 5baa7830..1f2775fb 100644
--- a/website/content/en/docs/talks/_index.md
+++ b/website/content/en/docs/talks/_index.md
@@ -21,7 +21,7 @@ Read the [slides](https://www.slideshare.net/AkihiroSuda/cncf-tagruntime-2022100
 ## 2023
 ### DevOps Toolkit 2023-02-09
 
-[Anders Björklund](https://github.com/afbjorklund) presented Lima in [The DevOps Toolkit Series](https://www.devopstoolkitseries.com/).
+[Anders Björklund](https://github.com/afbjorklund) presented Lima in The DevOps Toolkit Series.
 
 Watch the [video](https://www.youtube.com/watch?v=GDInFocQJTU).
 



Test patch:
diff --git a/pkg/jsonschemautil/jsonschemautil_test.go b/pkg/jsonschemautil/jsonschemautil_test.go
new file mode 100644
index 00000000..6301af7b
--- /dev/null
+++ b/pkg/jsonschemautil/jsonschemautil_test.go
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package jsonschemautil
+
+import (
+	"testing"
+
+	"gotest.tools/v3/assert"
+)
+
+func TestValidateValidInstance(t *testing.T) {
+	schema := "testdata/schema.json"
+	instance := "testdata/valid.yaml"
+	err := Validate(schema, instance)
+	assert.NilError(t, err)
+}
+
+func TestValidateInvalidInstance(t *testing.T) {
+	schema := "testdata/schema.json"
+	instance := "testdata/invalid.yaml"
+	err := Validate(schema, instance)
+	assert.ErrorContains(t, err, "jsonschema validation failed")
+}
diff --git a/pkg/jsonschemautil/testdata/invalid.yaml b/pkg/jsonschemautil/testdata/invalid.yaml
new file mode 100644
index 00000000..8538dd10
--- /dev/null
+++ b/pkg/jsonschemautil/testdata/invalid.yaml
@@ -0,0 +1,2 @@
+name: John Doe
+age: "30"
diff --git a/pkg/jsonschemautil/testdata/schema.json b/pkg/jsonschemautil/testdata/schema.json
new file mode 100644
index 00000000..33eb2d18
--- /dev/null
+++ b/pkg/jsonschemautil/testdata/schema.json
@@ -0,0 +1,11 @@
+{
+  "type": "object",
+  "properties": {
+    "name": {
+      "type": "string"
+    },
+    "age": {
+      "type": "integer"
+    }
+  }
+}
diff --git a/pkg/jsonschemautil/testdata/valid.yaml b/pkg/jsonschemautil/testdata/valid.yaml
new file mode 100644
index 00000000..be10e25b
--- /dev/null
+++ b/pkg/jsonschemautil/testdata/valid.yaml
@@ -0,0 +1,2 @@
+name: John Doe
+age: 30
diff --git a/pkg/limatmpl/abs_test.go b/pkg/limatmpl/abs_test.go
index b4f192f6..fbf7c19b 100644
--- a/pkg/limatmpl/abs_test.go
+++ b/pkg/limatmpl/abs_test.go
@@ -35,9 +35,9 @@ var useAbsLocatorsTestCases = []useAbsLocatorsTestCase{
 	{
 		"Flow style array of one base template",
 		"template://foo",
-		`base: {url: bar.yaml, digest: deadbeef}`,
+		`base: [{url: bar.yaml, digest: deadbeef}]`,
 		// not sure why the quotes around the URL were added; maybe because we don't copy the style from the source
-		`base: {url: 'template://bar.yaml', digest: deadbeef}`,
+		`base: [{url: 'template://bar.yaml', digest: deadbeef}]`,
 	},
 	{
 		"Flow style array of sequence of two base URLs",
diff --git a/pkg/limatmpl/embed_test.go b/pkg/limatmpl/embed_test.go
index a8e19291..e1db9ba2 100644
--- a/pkg/limatmpl/embed_test.go
+++ b/pkg/limatmpl/embed_test.go
@@ -374,13 +374,7 @@ provision:
 		`provision: [{script: "#! my script"}]`,
 	},
 	{
-		"ERROR base digest is not yet implemented (1)",
-		"",
-		"base: {url: base.yaml, digest: deafbad}",
-		"not yet implemented",
-	},
-	{
-		"ERROR base digest is not yet implemented (2)",
+		"ERROR base digest is not yet implemented",
 		"",
 		"base: [{url: base.yaml, digest: deafbad}]",
 		"not yet implemented",
diff --git a/pkg/limayaml/limayaml_test.go b/pkg/limayaml/limayaml_test.go
index a0320232..39c929ea 100644
--- a/pkg/limayaml/limayaml_test.go
+++ b/pkg/limayaml/limayaml_test.go
@@ -17,7 +17,7 @@ func dumpJSON(t *testing.T, d any) string {
 	return string(b)
 }
 
-const emptyYAML = "images: []\n"
+const emptyYAML = "{}\n"
 
 func TestEmptyYAML(t *testing.T) {
 	var y LimaYAML
@@ -27,7 +27,7 @@ func TestEmptyYAML(t *testing.T) {
 	assert.Equal(t, string(b), emptyYAML)
 }
 
-const defaultYAML = "images: []\n"
+const defaultYAML = "{}\n"
 
 func TestDefaultYAML(t *testing.T) {
 	bytes, err := os.ReadFile("default.yaml")
diff --git a/pkg/limayaml/marshal_test.go b/pkg/limayaml/marshal_test.go
index 29f2f72c..0f77b11f 100644
--- a/pkg/limayaml/marshal_test.go
+++ b/pkg/limayaml/marshal_test.go
@@ -28,7 +28,6 @@ func TestMarshalTilde(t *testing.T) {
 	// yaml will load ~ (or null) as null
 	// make sure that it is always quoted
 	assert.Equal(t, string(b), `---
-images: []
 mounts:
 - location: "~"
   writable: false