Repository: lima-vm/lima

Issue:
guestagent: `worthCheckingIPTables` logic seems incorrect
https://github.com/lima-vm/lima/blob/6c5ee213f3c699736f0780278eba0524bed83006/pkg/guestagent/guestagent_linux.go#L56-L74

https://github.com/lima-vm/lima/blob/6c5ee213f3c699736f0780278eba0524bed83006/pkg/guestagent/guestagent_linux.go#L85-L87

The former part sets `a.worthCheckingIPTables = true` when audit is enabled.
OTOH, the latter one sets `a.worthCheckingIPTables = true` when audit is disabled.



Discussion:
@ashwat287: When the agent starts and auditing is already enabled, the code sets `a.worthCheckingIPTables = true` and does not start `setWorthCheckingIPTablesRoutine`. The routine is the only place that later sets the flag to false or back to true on `NETFILTER_CFG ` events. Because it is not started in this path, `worthCheckingIPTables` stays true permanently. The code path that would use the cached `a.latestIPTables` (the else branch when the flag is false) never executes. Only when the agent itself enables auditing (initially disabled case) does it launch the routine and allow the flag to change state. This creates inconsistent behavior between “auditing already enabled” and “auditing just enabled” starts.



Solution patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c9ebd98c..8c462728 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -26,7 +26,7 @@ jobs:
     # Apparently, a binary built on a newer version of macOS can still run on
     # an older release of macOS without an error.
     # This is quite different from Linux and glibc.
-    runs-on: macos-15
+    runs-on: macos-26
     timeout-minutes: 20
     steps:
     - name: "Show xcode and SDK version"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4c45aaa3..2483e683 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -69,7 +69,7 @@ jobs:
     timeout-minutes: 30
     strategy:
       matrix:
-        runs-on: [ubuntu-24.04, macos-15, windows-2025]
+        runs-on: [ubuntu-24.04, macos-26, windows-2025]
     runs-on: ${{ matrix.runs-on }}
     steps:
     - name: Force git to use LF
@@ -240,7 +240,7 @@ jobs:
     - name: Make
       run: make
     - name: Install
-      run: make install
+      run: sudo make install
     - name: Validate jsonschema
       run: make schema-limayaml.json
     - name: Validate templates
@@ -365,7 +365,7 @@ jobs:
     - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00  # v6.0.0
       with:
         go-version: 1.25.x
-    - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809  # v4.2.4
+    - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830  # v4.3.0
       with:
         path: ~/.cache/lima/download
         key: ${{ runner.os }}-colima-${{ matrix.colima-version }}
@@ -411,7 +411,7 @@ jobs:
     - name: Make
       run: make
     - name: Install
-      run: make install
+      run: sudo make install
     - name: "Adjust LIMACTL_CREATE_ARGS"
       run: echo "LIMACTL_CREATE_ARGS=${LIMACTL_CREATE_ARGS} --vm-type=qemu --network=lima:shared" >>$GITHUB_ENV
     - name: "Inject `no_timer_check` to kernel cmdline"
@@ -508,7 +508,7 @@ jobs:
     - name: Make
       run: make
     - name: Install
-      run: make install
+      run: sudo make install
     - name: Cache image used by templates/${{ matrix.template }}
       uses: ./.github/actions/setup_cache_for_template
       with:
@@ -546,7 +546,9 @@ jobs:
         make binaries install
     - name: Install Lima
       # gomodjail depends on symbols
-      run: make KEEP_SYMBOLS=1 binaries install
+      run: |
+        make KEEP_SYMBOLS=1 binaries
+        sudo make install
     - name: Cache image used by templates/default.yaml
       uses: ./.github/actions/setup_cache_for_template
       with:
diff --git a/.ls-lint.yml b/.ls-lint.yml
index 972dbf2b..10c75647 100644
--- a/.ls-lint.yml
+++ b/.ls-lint.yml
@@ -16,9 +16,6 @@ ls:
     # valid names are `show-ssh.go`, `show-ssh_windows.go` or `show-ssh_test.go`
     .go: lowercase
 
-  docs:
-    .md: kebab-case
-
   templates:
     # _default and _images have leading underscores
     .dir: lowercase
diff --git a/Makefile b/Makefile
index 8c99b56b..673888e7 100644
--- a/Makefile
+++ b/Makefile
@@ -113,6 +113,9 @@ help-targets:
 	@echo  '- lima                      : Copy lima, and lima.bat'
 	@echo  '- helpers                   : Copy nerdctl.lima, apptainer.lima, docker.lima, podman.lima, and kubectl.lima'
 	@echo
+	@echo  'Targets for files in _output/libexec/lima/:'
+	@echo  '- limactl-plugins           : Build limactl-* CLI plugins'
+	@echo
 	@echo  'Targets for files in _output/share/lima/:'
 	@echo  '- guestagents               : Build guestagents'
 	@echo  '- native-guestagent         : Build guestagent for native arch'
@@ -174,7 +177,7 @@ CONFIG_GUESTAGENT_COMPRESS=y
 
 ################################################################################
 .PHONY: binaries
-binaries: limactl helpers guestagents \
+binaries: limactl helpers limactl-plugins guestagents \
 	templates template_experimentals \
 	documentation create-links-in-doc-dir
 
@@ -280,20 +283,26 @@ ifeq ($(GOOS),darwin)
 	codesign -f -v --entitlements vz.entitlements -s - $@
 endif
 
-DRIVER_INSTALL_DIR := _output/libexec/lima
+LIBEXEC_LIMA := _output/libexec/lima
+
+limactl-plugins: $(LIBEXEC_LIMA)/limactl-mcp$(exe)
+
+$(LIBEXEC_LIMA)/limactl-mcp$(exe): $(call dependencies_for_cmd,limactl-mcp) $$(call force_build,$$@)
+	@mkdir -p $(LIBEXEC_LIMA)
+	$(ENVS_$@) $(GO_BUILD) -o $@ ./cmd/limactl-mcp
 
 .PHONY: additional-drivers
 additional-drivers:
-	@mkdir -p $(DRIVER_INSTALL_DIR)
+	@mkdir -p $(LIBEXEC_LIMA)
 	@for drv in $(ADDITIONAL_DRIVERS); do \
 		echo "Building $$drv as external"; \
 		if [ "$(GOOS)" = "windows" ]; then \
-			$(GO_BUILD) -o $(DRIVER_INSTALL_DIR)/lima-driver-$$drv.exe ./cmd/lima-driver-$$drv; \
+			$(GO_BUILD) -o $(LIBEXEC_LIMA)/lima-driver-$$drv.exe ./cmd/lima-driver-$$drv; \
 		else \
-			$(GO_BUILD) -o $(DRIVER_INSTALL_DIR)/lima-driver-$$drv ./cmd/lima-driver-$$drv; \
+			$(GO_BUILD) -o $(LIBEXEC_LIMA)/lima-driver-$$drv ./cmd/lima-driver-$$drv; \
 			fi; \
 		if [ "$$drv" = "vz" ] && [ "$(GOOS)" = "darwin" ]; then \
-			codesign -f -v --entitlements vz.entitlements -s - $(DRIVER_INSTALL_DIR)/lima-driver-vz; \
+			codesign -f -v --entitlements vz.entitlements -s - $(LIBEXEC_LIMA)/lima-driver-vz; \
 		fi; \
 	done
 
@@ -469,7 +478,7 @@ endif
 # Set limactl.md as explicit dependency.
 # It's uncertain how many docsy pages will be generated by `make`,
 # because `limactl` generates them without corresponding source code for the docsy pages.
-docsy: website/_output/docsy/limactl.md
+docsy: website/_output/docsy/limactl.md website/_output/docsy-mcp/mcp.md
 website/_output/docsy/limactl.md: _output/bin/limactl$(exe)
 	@mkdir -p website/_output/docsy
 ifeq ($(native_compiling),true)
@@ -477,6 +486,11 @@ ifeq ($(native_compiling),true)
 	$< generate-doc --type docsy website/_output/docsy \
 		--output _output --prefix $(PREFIX)
 endif
+website/_output/docsy-mcp/mcp.md: _output/libexec/lima/limactl-mcp$(exe)
+	@mkdir -p website/_output/docsy-mcp
+ifeq ($(native_compiling),true)
+	$< generate-doc website/_output/docsy-mcp
+endif
 
 ################################################################################
 default-template.yaml: _output/bin/limactl$(exe)
@@ -525,6 +539,7 @@ uninstall:
 		"$(DEST)/share/man/man1/limactl"*".1" \
 		"$(DEST)/share/lima" \
 		"$(DEST)/share/doc/lima" \
+		"$(DEST)/libexec/lima/limactl-mcp$(exe)" \
 		"$(DEST)/libexec/lima/lima-driver-qemu$(exe)" \
 		"$(DEST)/libexec/lima/lima-driver-vz$(exe)" \
 		"$(DEST)/libexec/lima/lima-driver-wsl2$(exe)"
diff --git a/cmd/lima-guestagent/daemon_linux.go b/cmd/lima-guestagent/daemon_linux.go
index 748196f0..87e4a4fe 100644
--- a/cmd/lima-guestagent/daemon_linux.go
+++ b/cmd/lima-guestagent/daemon_linux.go
@@ -16,6 +16,7 @@ import (
 	"github.com/lima-vm/lima/v2/pkg/guestagent"
 	"github.com/lima-vm/lima/v2/pkg/guestagent/api/server"
 	"github.com/lima-vm/lima/v2/pkg/guestagent/serialport"
+	"github.com/lima-vm/lima/v2/pkg/guestagent/ticker"
 	"github.com/lima-vm/lima/v2/pkg/portfwdserver"
 )
 
@@ -52,17 +53,20 @@ func daemonAction(cmd *cobra.Command, _ []string) error {
 	if os.Geteuid() != 0 {
 		return errors.New("must run as the root user")
 	}
-	logrus.Infof("event tick: %v", tick)
 
-	newTicker := func() (<-chan time.Time, func()) {
-		// TODO: use an equivalent of `bpftrace -e 'tracepoint:syscalls:sys_*_bind { printf("tick\n"); }')`,
-		// without depending on `bpftrace` binary.
-		// The agent binary will need CAP_BPF file cap.
-		ticker := time.NewTicker(tick)
-		return ticker.C, ticker.Stop
+	logrus.Infof("event tick: %v", tick)
+	simpleTicker := ticker.NewSimpleTicker(time.NewTicker(tick))
+	tickerInst := simpleTicker
+	// See /sys/kernel/debug/tracing/available_events for the list of available tracepoints
+	tracepoints := []string{"syscalls:sys_exit_bind"}
+	if ebpfTicker, err := ticker.NewEbpfTicker(tracepoints); err != nil {
+		logrus.WithError(err).Warn("failed to create eBPF ticker, falling back to simple ticker")
+	} else {
+		logrus.Infof("using eBPF ticker with tracepoints: %v", tracepoints)
+		tickerInst = ticker.NewCompoundTicker(simpleTicker, ebpfTicker)
 	}
 
-	agent, err := guestagent.New(ctx, newTicker, tick*20)
+	agent, err := guestagent.New(ctx, tickerInst, tick*20)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/limactl-mcp/main.go b/cmd/limactl-mcp/main.go
new file mode 100644
index 00000000..0195646d
--- /dev/null
+++ b/cmd/limactl-mcp/main.go
@@ -0,0 +1,253 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+	"golang.org/x/text/cases"
+	"golang.org/x/text/language"
+
+	"github.com/lima-vm/lima/v2/pkg/limactlutil"
+	"github.com/lima-vm/lima/v2/pkg/mcp/toolset"
+	"github.com/lima-vm/lima/v2/pkg/version"
+)
+
+func main() {
+	if err := newApp().Execute(); err != nil {
+		logrus.Fatal(err)
+	}
+}
+
+func newApp() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:           "limactl-mcp",
+		Short:         "Model Context Protocol plugin for Lima (EXPERIMENTAL)",
+		Version:       strings.TrimPrefix(version.Version, "v"),
+		SilenceUsage:  true,
+		SilenceErrors: true,
+	}
+	cmd.AddCommand(
+		newMcpInfoCommand(),
+		newMcpServeCommand(),
+		newMcpGenDocCommand(),
+		// TODO: `limactl-mcp configure gemini` ?
+	)
+	return cmd
+}
+
+func newServer() *mcp.Server {
+	impl := &mcp.Implementation{
+		Name:    "lima",
+		Title:   "Lima VM, for sandboxing local command executions and file I/O operations",
+		Version: version.Version,
+	}
+	serverOpts := &mcp.ServerOptions{
+		Instructions: `This MCP server provides tools for sandboxing local command executions and file I/O operations,
+by wrapping them in Lima VM (https://lima-vm.io).
+
+Use these tools to avoid accidentally executing malicious codes directly on the host.
+`,
+	}
+	if runtime.GOOS != "linux" {
+		serverOpts.Instructions += fmt.Sprintf(`
+
+NOTE: the guest OS of the VM is Linux, while the host OS is %s.
+`, cases.Title(language.English).String(runtime.GOOS))
+	}
+	return mcp.NewServer(impl, serverOpts)
+}
+
+func newMcpInfoCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "info",
+		Short: "Show information about the MCP server",
+		Args:  cobra.NoArgs,
+		RunE:  mcpInfoAction,
+	}
+	return cmd
+}
+
+func mcpInfoAction(cmd *cobra.Command, _ []string) error {
+	ctx := cmd.Context()
+	info, err := inspectInfo(ctx)
+	if err != nil {
+		return err
+	}
+	j, err := json.MarshalIndent(info, "", "    ")
+	if err != nil {
+		return err
+	}
+	_, err = fmt.Fprint(cmd.OutOrStdout(), string(j))
+	return err
+}
+
+func inspectInfo(ctx context.Context) (*Info, error) {
+	ts, err := toolset.New("")
+	if err != nil {
+		return nil, err
+	}
+	server := newServer()
+	if err = ts.RegisterServer(server); err != nil {
+		return nil, err
+	}
+	serverTransport, clientTransport := mcp.NewInMemoryTransports()
+	serverSession, err := server.Connect(ctx, serverTransport, nil)
+	if err != nil {
+		return nil, err
+	}
+	client := mcp.NewClient(&mcp.Implementation{Name: "client"}, nil)
+	clientSession, err := client.Connect(ctx, clientTransport, nil)
+	if err != nil {
+		return nil, err
+	}
+	toolsResult, err := clientSession.ListTools(ctx, &mcp.ListToolsParams{})
+	if err != nil {
+		return nil, err
+	}
+	if err = clientSession.Close(); err != nil {
+		return nil, err
+	}
+	if err = serverSession.Wait(); err != nil {
+		return nil, err
+	}
+	info := &Info{
+		Tools: toolsResult.Tools,
+	}
+	return info, nil
+}
+
+type Info struct {
+	Tools []*mcp.Tool `json:"tools"`
+}
+
+func newMcpServeCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "serve INSTANCE",
+		Short: "Serve MCP over stdio",
+		Long: `Serve MCP over stdio.
+
+Expected to be executed via an AI agent, not by a human`,
+		Args: cobra.MaximumNArgs(1),
+		RunE: mcpServeAction,
+	}
+	return cmd
+}
+
+func mcpServeAction(cmd *cobra.Command, args []string) error {
+	ctx := cmd.Context()
+	instName := "default"
+	if len(args) > 0 {
+		instName = args[0]
+	}
+	limactl, err := limactlutil.Path()
+	if err != nil {
+		return err
+	}
+	// FIXME: We can not use store.Inspect() here because it requires VM drivers to be compiled in.
+	// https://github.com/lima-vm/lima/pull/3744#issuecomment-3289274347
+	inst, err := limactlutil.Inspect(ctx, limactl, instName)
+	if err != nil {
+		return err
+	}
+	if len(inst.Errors) != 0 {
+		return errors.Join(inst.Errors...)
+	}
+	ts, err := toolset.New(limactl)
+	if err != nil {
+		return err
+	}
+	server := newServer()
+	if err = ts.RegisterServer(server); err != nil {
+		return err
+	}
+	if err = ts.RegisterInstance(ctx, inst); err != nil {
+		return err
+	}
+	transport := &mcp.StdioTransport{}
+	return server.Run(ctx, transport)
+}
+
+func newMcpGenDocCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:    "generate-doc DIR",
+		Short:  "Generate documentation pages",
+		Args:   cobra.MinimumNArgs(1),
+		RunE:   mcpGenDocAction,
+		Hidden: true,
+	}
+	return cmd
+}
+
+func mcpGenDocAction(cmd *cobra.Command, args []string) error {
+	ctx := cmd.Context()
+	dir := args[0]
+	if err := os.MkdirAll(dir, 0o755); err != nil {
+		return err
+	}
+	fName := filepath.Join(dir, "mcp.md")
+	f, err := os.Create(fName)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	fmt.Fprint(f, `---
+title: MCP tools
+weight: 99
+---
+Lima implements the "MCP Sandbox Interface" (tentative name):
+https://pkg.go.dev/github.com/lima-vm/lima/v2/pkg/mcp/msi
+
+MCP Sandbox Interface defines MCP (Model Context Protocol) tools
+that can be used for reading, writing, and executing local files
+with an appropriate sandboxing technology, such as Lima.
+
+The sandboxing technology can be more secure and/or efficient than
+the default tools provided by an AI agent.
+
+MCP Sandbox Interface was inspired by
+[Google Gemini CLI's built-in tools](https://github.com/google-gemini/gemini-cli/tree/main/docs/tools).
+
+`)
+	info, err := inspectInfo(ctx)
+	if err != nil {
+		return err
+	}
+	for _, tool := range info.Tools {
+		fmt.Fprintf(f, "## `%s`\n\n", tool.Name)
+		if tool.Title != "" {
+			fmt.Fprintf(f, "### Title\n\n%s\n\n", tool.Title)
+		}
+		if tool.Description != "" {
+			fmt.Fprintf(f, "### Description\n\n%s\n\n", tool.Description)
+		}
+		if tool.InputSchema != nil {
+			fmt.Fprint(f, "### Input Schema\n\n")
+			schema, err := json.MarshalIndent(tool.InputSchema, "", "    ")
+			if err != nil {
+				return err
+			}
+			fmt.Fprintf(f, "```json\n%s\n```\n\n", string(schema))
+		}
+		if tool.OutputSchema != nil {
+			fmt.Fprint(f, "### Output Schema\n\n")
+			schema, err := json.MarshalIndent(tool.OutputSchema, "", "    ")
+			if err != nil {
+				return err
+			}
+			fmt.Fprintf(f, "```json\n%s\n```\n\n", string(schema))
+		}
+	}
+	return f.Close()
+}
diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go
index 20b53237..39b93421 100644
--- a/cmd/limactl/main.go
+++ b/cmd/limactl/main.go
@@ -4,11 +4,9 @@
 package main
 
 import (
-	"context"
 	"errors"
 	"fmt"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"runtime"
 	"strings"
@@ -23,6 +21,7 @@ import (
 	"github.com/lima-vm/lima/v2/pkg/fsutil"
 	"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
 	"github.com/lima-vm/lima/v2/pkg/osutil"
+	"github.com/lima-vm/lima/v2/pkg/plugin"
 	"github.com/lima-vm/lima/v2/pkg/version"
 )
 
@@ -49,13 +48,12 @@ func main() {
 		}
 	}
 	rootCmd := newApp()
-	if err := executeWithPluginSupport(rootCmd, os.Args[1:]); err != nil {
-		server.StopAllExternalDrivers()
-		handleExitError(err)
+	err := executeWithPluginSupport(rootCmd, os.Args[1:])
+	server.StopAllExternalDrivers()
+	osutil.HandleExitError(err)
+	if err != nil {
 		logrus.Fatal(err)
 	}
-
-	server.StopAllExternalDrivers()
 }
 
 func newApp() *cobra.Command {
@@ -120,12 +118,6 @@ func newApp() *cobra.Command {
 			return fmt.Errorf("unsupported log-format: %q", logFormat)
 		}
 
-		debug, _ := cmd.Flags().GetBool("debug")
-		if debug {
-			logrus.SetLevel(logrus.DebugLevel)
-			debugutil.Debug = true
-		}
-
 		if osutil.IsBeingRosettaTranslated() && cmd.Parent().Name() != "completion" && cmd.Name() != "generate-doc" && cmd.Name() != "validate" {
 			// running under rosetta would provide inappropriate runtime.GOARCH info, see: https://github.com/lima-vm/lima/issues/543
 			// allow commands that are used for packaging to run under rosetta to allow cross-architecture builds
@@ -165,6 +157,8 @@ func newApp() *cobra.Command {
 	}
 	rootCmd.AddGroup(&cobra.Group{ID: "basic", Title: "Basic Commands:"})
 	rootCmd.AddGroup(&cobra.Group{ID: "advanced", Title: "Advanced Commands:"})
+	rootCmd.AddGroup(&cobra.Group{ID: "plugin", Title: "Available Plugins (Experimental):"})
+
 	rootCmd.AddCommand(
 		newCreateCommand(),
 		newStartCommand(),
@@ -201,79 +195,45 @@ func newApp() *cobra.Command {
 	return rootCmd
 }
 
-func handleExitError(err error) {
-	if err == nil {
-		return
-	}
-
-	var exitErr *exec.ExitError
-	if errors.As(err, &exitErr) {
-		os.Exit(exitErr.ExitCode()) //nolint:revive // it's intentional to call os.Exit in this function
-		return
-	}
-}
-
-// executeWithPluginSupport handles command execution with plugin support.
 func executeWithPluginSupport(rootCmd *cobra.Command, args []string) error {
-	if len(args) > 0 {
-		cmd, _, err := rootCmd.Find(args)
-		if err != nil || cmd == rootCmd {
-			// Function calls os.Exit() if it found and executed the plugin
-			runExternalPlugin(rootCmd.Context(), args[0], args[1:])
+	rootCmd.SetArgs(args)
+
+	if err := rootCmd.ParseFlags(args); err == nil {
+		if debug, _ := rootCmd.Flags().GetBool("debug"); debug {
+			logrus.SetLevel(logrus.DebugLevel)
+			debugutil.Debug = true
 		}
 	}
 
-	rootCmd.SetArgs(args)
+	addPluginCommands(rootCmd)
+
 	return rootCmd.Execute()
 }
 
-func runExternalPlugin(ctx context.Context, name string, args []string) {
-	if ctx == nil {
-		ctx = context.Background()
-	}
-
-	if err := updatePathEnv(); err != nil {
-		logrus.Warnf("failed to update PATH environment: %v", err)
-		// PATH update failure shouldn't prevent plugin execution
-	}
-
-	externalCmd := "limactl-" + name
-	execPath, err := exec.LookPath(externalCmd)
+func addPluginCommands(rootCmd *cobra.Command) {
+	plugins, err := plugin.DiscoverPlugins()
 	if err != nil {
+		logrus.Warnf("Failed to discover plugins: %v", err)
 		return
 	}
 
-	cmd := exec.CommandContext(ctx, execPath, args...)
-	cmd.Stdin = os.Stdin
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	cmd.Env = os.Environ()
-
-	err = cmd.Run()
-	handleExitError(err)
-	if err == nil {
-		os.Exit(0) //nolint:revive // it's intentional to call os.Exit in this function
-	}
-	logrus.Fatalf("external command %q failed: %v", execPath, err)
-}
-
-func updatePathEnv() error {
-	exe, err := os.Executable()
-	if err != nil {
-		return fmt.Errorf("failed to get executable path: %w", err)
-	}
+	for _, p := range plugins {
+		pluginName := p.Name
+		pluginCmd := &cobra.Command{
+			Use:                pluginName,
+			Short:              p.Description,
+			GroupID:            "plugin",
+			DisableFlagParsing: true,
+			Run: func(cmd *cobra.Command, args []string) {
+				plugin.RunExternalPlugin(cmd.Context(), pluginName, args)
+			},
+		}
 
-	binDir := filepath.Dir(exe)
-	currentPath := os.Getenv("PATH")
-	newPath := binDir + string(filepath.ListSeparator) + currentPath
+		pluginCmd.SilenceUsage = true
+		pluginCmd.SilenceErrors = true
 
-	if err := os.Setenv("PATH", newPath); err != nil {
-		return fmt.Errorf("failed to set PATH environment: %w", err)
+		rootCmd.AddCommand(pluginCmd)
 	}
-
-	logrus.Debugf("updated PATH to prioritize %s", binDir)
-
-	return nil
 }
 
 // WrapArgsError annotates cobra args error with some context, so the error message is more user-friendly.
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000..5dfb6367
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1 @@
+Moved to <https://lima-vm.io/docs/>
diff --git a/docs/deprecated.md b/docs/deprecated.md
deleted file mode 100644
index cd79541a..00000000
--- a/docs/deprecated.md
+++ /dev/null
@@ -1 +0,0 @@
-Moved to <https://lima-vm.io/docs/releases/deprecated/>
diff --git a/docs/experimental.md b/docs/experimental.md
deleted file mode 100644
index bcad5cb1..00000000
--- a/docs/experimental.md
+++ /dev/null
@@ -1 +0,0 @@
-Moved to <https://lima-vm.io/docs/releases/experimental/>
diff --git a/docs/internals.md b/docs/internals.md
deleted file mode 100644
index 6c6bfebb..00000000
--- a/docs/internals.md
+++ /dev/null
@@ -1 +0,0 @@
-Moved to <https://lima-vm.io/docs/dev/internals/>
diff --git a/docs/mount.md b/docs/mount.md
deleted file mode 100644
index 1e7b4d98..00000000
--- a/docs/mount.md
+++ /dev/null
@@ -1 +0,0 @@
-Moved to <https://lima-vm.io/docs/config/mount/>
diff --git a/docs/multi-arch.md b/docs/multi-arch.md
deleted file mode 100644
index 47131e7d..00000000
--- a/docs/multi-arch.md
+++ /dev/null
@@ -1 +0,0 @@
-Moved to <https://lima-vm.io/docs/config/multi-arch/>
diff --git a/docs/network.md b/docs/network.md
deleted file mode 100644
index 955be86a..00000000
--- a/docs/network.md
+++ /dev/null
@@ -1 +0,0 @@
-Moved to <https://lima-vm.io/docs/config/network/>
diff --git a/docs/talks.md b/docs/talks.md
deleted file mode 100644
index 9104bcf8..00000000
--- a/docs/talks.md
+++ /dev/null
@@ -1 +0,0 @@
-Moved to <https://lima-vm.io/docs/talks/>
diff --git a/docs/vmtype.md b/docs/vmtype.md
deleted file mode 100644
index 8cc8fff1..00000000
--- a/docs/vmtype.md
+++ /dev/null
@@ -1 +0,0 @@
-Moved to <https://lima-vm.io/docs/config/vmtype/>
diff --git a/go.mod b/go.mod
index bce4a3c3..2b5dba95 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
 	github.com/apparentlymart/go-cidr v1.1.0
 	github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e
 	github.com/cheggaaa/pb/v3 v3.1.7 // gomodjail:unconfined
+	github.com/cilium/ebpf v0.19.0 // gomodjail:unconfined
 	github.com/containerd/continuity v0.4.5
 	github.com/containers/gvisor-tap-vsock v0.8.7 // gomodjail:unconfined
 	github.com/coreos/go-semver v0.3.1
@@ -31,9 +32,11 @@ require (
 	github.com/mdlayher/vsock v1.2.1 // gomodjail:unconfined
 	github.com/miekg/dns v1.1.68 // gomodjail:unconfined
 	github.com/mikefarah/yq/v4 v4.47.2
+	github.com/modelcontextprotocol/go-sdk v0.7.0
 	github.com/nxadm/tail v1.4.11 // gomodjail:unconfined
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
+	github.com/pkg/sftp v1.13.9
 	github.com/rjeczalik/notify v0.9.3
 	github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
 	github.com/sethvargo/go-password v0.3.1
@@ -105,13 +108,13 @@ require (
 	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
 	github.com/pierrec/lz4/v4 v4.1.22 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
-	github.com/pkg/sftp v1.13.9 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
 	// gomodjail:unconfined
 	github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
+	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	github.com/yuin/gopher-lua v1.1.1 // indirect
 	golang.org/x/crypto v0.42.0 // indirect
 	golang.org/x/mod v0.27.0 // indirect
@@ -135,6 +138,7 @@ require (
 
 require (
 	github.com/go-ini/ini v1.67.0 // indirect
+	github.com/google/jsonschema-go v0.3.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
diff --git a/go.sum b/go.sum
index 013b0d6e..604ebd66 100644
--- a/go.sum
+++ b/go.sum
@@ -34,6 +34,8 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMU
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
 github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ=
+github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao=
+github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY=
 github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
 github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
 github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
@@ -96,6 +98,8 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF
 github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
 github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
 github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
+github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
 github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
@@ -118,6 +122,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
 github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
+github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
+github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
 github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
 github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
@@ -142,6 +148,9 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
 github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
+github.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9XlpVA=
+github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
+github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@@ -176,6 +185,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
 github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
+github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
+github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
 github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
 github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
 github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
@@ -191,6 +202,8 @@ github.com/mikefarah/yq/v4 v4.47.2 h1:Jb5fHlvgK5eeaPbreG9UJs1E5w6l5hUzXjeaY6LTTW
 github.com/mikefarah/yq/v4 v4.47.2/go.mod h1:ulYbZUzGJsBDDwO5ohvk/KOW4vW5Iddd/DBeAY1Q09g=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modelcontextprotocol/go-sdk v0.7.0 h1:XEQfn3bDx2cAdSUKty3tYEMll5dtRgBUDX88Q65fai0=
+github.com/modelcontextprotocol/go-sdk v0.7.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -266,6 +279,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/
 github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
diff --git a/hack/codesign/debugserver b/hack/codesign/debugserver
new file mode 100755
index 00000000..e916f48d
--- /dev/null
+++ b/hack/codesign/debugserver
@@ -0,0 +1,113 @@
+#!/bin/bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
+# # `hack/codesign/debugserver`
+#
+# This script wraps the LLDB `debugserver` to `codesign` the target executable.
+# This is needed for [Delve](https://github.com/go-delve/delve) to work properly
+# on macOS with the virtualization framework.
+#
+# ## How to use this script with Delve
+#
+# ### Use `DELVE_DEBUGSERVER_PATH` environment variable
+#
+# Override `debugserver` by setting the `DELVE_DEBUGSERVER_PATH` environment variable.
+# Ref: [Environment variables - Using Delve](https://github.com/go-delve/delve/blob/master/Documentation/usage/README.md#environment-variables)
+#
+# e.g. in `.vscode/launch.json`:
+# ```jsonc
+# {
+#     "version": "0.2.0",
+#     "configurations": [
+#         {
+#             "name": "hostagent for input instance",
+#             "type": "go",
+#             "request": "launch",
+#             "mode": "debug",
+#             // Use integratedTerminal to stop using ctrl+C
+#             "console": "integratedTerminal",
+#             "program": "${workspaceFolder}/cmd/limactl",
+#             "buildFlags": [
+#                 "-ldflags=-X github.com/lima-vm/lima/pkg/version.Version=2.0.0-alpha.0",
+#             ],
+#             "env": {
+#                 "CGO_ENABLED": "1",
+#                 "DELVE_DEBUGSERVER_PATH": "${workspaceFolder}/hack/codesign/debugserver",
+#                 "LIMA_SSH_PORT_FORWARDER": "true",
+#             },
+#             "cwd": "${userHome}/.lima/${input:targetInstance}",
+#             "args": [
+#                 "--debug",
+#                 "hostagent",
+#                 "--pidfile", "ha.pid",
+#                 "--socket", "ha.sock",
+#                 "--guestagent", "${workspaceFolder}/_output/share/lima/lima-guestagent.Linux-aarch64",
+#                 "${input:targetInstance}"
+#             ],
+# 		},
+#     ],
+#     "inputs": [
+#         {
+#             "id": "targetInstance",
+#             "type": "promptString",
+#             "description": "Input target instance parameter for `limactl` command",
+#         }
+#     ]
+# }
+# ```
+#
+# ### Override `debugserver` in the PATH
+#
+# You can also override the `debugserver` in the PATH environment variable.
+#
+# e.g. in `.vscode/launch.json`:
+# ```diff
+#              "env": {
+#                  "CGO_ENABLED": "1",
+# -                "DELVE_DEBUGSERVER_PATH": "${workspaceFolder}/hack/codesign/debugserver",
+# +                "PATH": "${workspaceFolder}/hack/codesign:${env:PATH}",
+#              },
+# ```
+
+set -eu -o pipefail
+
+script_dir="$(dirname "$0")"
+
+# Codesign the target executable if vz.entitlements exists
+entitlements="${script_dir}/../../vz.entitlements"
+if [ -f "${entitlements}" ]; then
+	prev_arg=
+	for arg in "$@"; do
+		# find the target executable in the args next to "--"
+		if [ "${prev_arg}" = "--" ]; then
+			codesign --entitlements "${entitlements}" --force -s - -v "${arg}" || {
+				echo "error: codesign failed" >&2
+				exit 1
+			}
+			break
+		fi
+		prev_arg="${arg}"
+	done
+fi
+
+# Simulate how Delve locates debugserver
+# https://github.com/go-delve/delve/blob/65a6830eb7f0b882e0c52df0ef9585945ed55470/pkg/proc/gdbserial/gdbserver.go#L103-L129
+# 1. PATH (in this script, excluding the directory of this script to avoid recursion)
+candidate_paths="$(echo "${PATH}" | tr ':' '\n' | grep --fixed-strings --invert-match --line-regexp "${script_dir}" | paste -sd: -)"
+# 2. CommandLineTools LLDB path
+CLT_path="/Library/Developer/CommandLineTools"
+candidate_paths="${candidate_paths}:${CLT_path}/Library/PrivateFrameworks/LLDB.framework/Versions/A/Resources/"
+# 3. Xcode LLDB path (if Xcode is installed)
+if developer_dir=$(xcode-select --print-path 2>/dev/null) && [ "${developer_dir}" != "${CLT_path}" ]; then
+	# Xcode is installed
+	candidate_paths="${candidate_paths}:${developer_dir}/../SharedFrameworks/LLDB.framework/Versions/A/Resources/"
+fi
+# Find debugserver in the candidate paths
+debugserver=$(PATH="${candidate_paths}" command -v debugserver) || {
+	echo "error: debugserver not found" >&2
+	exit 1
+}
+# Execute debugserver with the original arguments
+exec "${debugserver}" "$@"
diff --git a/hack/test-port-forwarding.pl b/hack/test-port-forwarding.pl
index 68394ae9..9e98db43 100755
--- a/hack/test-port-forwarding.pl
+++ b/hack/test-port-forwarding.pl
@@ -16,6 +16,7 @@ use strict;
 use warnings;
 
 use Config qw(%Config);
+use File::Spec::Functions qw(catfile);
 use IO::Handle qw();
 use Socket qw(inet_ntoa);
 use Sys::Hostname qw(hostname);
@@ -33,6 +34,11 @@ if ($ipv4 eq "127.0.0.1" && $Config{osname} eq "darwin") {
     chomp $ipv4;
 }
 
+my $instDir = qx(limactl list "$instance" --yq .dir);
+chomp $instDir;
+# platform independent way to add trailing path separator
+my $sockDir = catfile($instDir, "sock", "");
+
 # If $instance is a filename, add our portForwards to it to enable testing
 if (-f $instance) {
     open(my $fh, "+< $instance") or die "Can't open $instance for read/write: $!";
@@ -94,14 +100,17 @@ while (<DATA>) {
     s/sshLocalPort/$sshLocalPort/g;
     s/ipv4/$ipv4/g;
     s/ipv6/$ipv6/g;
+    s/sockDir\//$sockDir/g;
     # forward: 127.0.0.1 899 → 127.0.0.1 799
     # ignore: 127.0.0.2 8888
-    /^(forward|ignore):\s+([0-9.:]+)\s+(\d+)(?:\s+→)?(?:\s+([0-9.:]+)(?:\s+(\d+))?)?/;
+    /^(forward|ignore):\s+([0-9.:]+)\s+(\d+)(?:\s+→)?(?:\s+(?:([0-9.:]+)(?:\s+(\d+))|(\S+))?)?/;
     die "Cannot parse test '$_'" unless $1;
-    my %test; @test{qw(mode guest_ip guest_port host_ip host_port)} = ($1, $2, $3, $4, $5);
+    my %test; @test{qw(mode guest_ip guest_port host_ip host_port host_socket)} = ($1, $2, $3, $4, $5, $6);
+     
     $test{host_ip} ||= "127.0.0.1";
     $test{host_port} ||= $test{guest_port};
-    if ($test{mode} eq "forward" && $test{host_port} < 1024 && $Config{osname} ne "darwin") {
+    $test{host_socket} ||= "";
+    if ($test{mode} eq "forward" && $test{host_socket} eq "" && $test{host_port} < 1024 && $Config{osname} ne "darwin") {
         printf "🚧 Not supported on $Config{osname}: # $_\n";
         next;
     }
@@ -113,9 +122,13 @@ while (<DATA>) {
         printf "🚧 Not supported for $instanceType machines: # $_\n";
         next;
     }
+    if ($test{host_socket} ne "" && $Config{osname} eq "cygwin") {
+        printf "🚧 Not supported on $Config{osname}: # $_\n";
+        next;
+    }
 
     my $remote = JoinHostPort($test{guest_ip},$test{guest_port});
-    my $local = JoinHostPort($test{host_ip},$test{host_port});
+    my $local = $test{host_socket} eq "" ? JoinHostPort($test{host_ip},$test{host_port}) : $test{host_socket};
     if ($test{mode} eq "ignore") {
         $test{log_msg} = "Not forwarding TCP $remote";
     }
@@ -163,7 +176,7 @@ sleep 5;
 # Try to reach each listener from the host
 foreach my $test (@test) {
     next if $test->{host_port} == $sshLocalPort;
-    my $nc = "nc -w 1 $test->{host_ip} $test->{host_port}";
+    my $nc = $test->{host_socket} eq "" ? "nc -w 1 $test->{host_ip} $test->{host_port}" : "nc -w 1 -U $test->{host_socket}";
     open(my $netcat, "| $nc") or die "Can't run '$nc': $!";
     print $netcat "$test->{log_msg}\n";
     # Don't check for errors on close; macOS nc seems to return non-zero exit code even on success
@@ -175,7 +188,7 @@ open(my $log, "< $ha_log") or die "Can't read $ha_log: $!";
 seek($log, $ha_log_size, 0) or die "Can't seek $ha_log to $ha_log_size: $!";
 my %seen;
 while (<$log>) {
-    $seen{$1}++ if /(Forwarding TCP from .*? to (\d.*?|\[.*?\]):\d+)/;
+    $seen{$1}++ if /(Forwarding TCP from .*? to ((\d.*?|\[.*?\]):\d+|\/[^"]+))/;
     $seen{$1}++ if /(Not forwarding TCP .*?:\d+)/;
 }
 close $log or die;
@@ -342,3 +355,8 @@ portForwards:
 - guestIPMustBeZero: true
   guestPort: 8888
   hostIP: 0.0.0.0
+
+- guestPort: 5000
+  hostSocket: port5000.sock
+
+  # forward: 127.0.0.1 5000 → sockDir/port5000.sock
diff --git a/hack/test-templates.sh b/hack/test-templates.sh
index f0727a8a..e1b97b5e 100755
--- a/hack/test-templates.sh
+++ b/hack/test-templates.sh
@@ -61,6 +61,7 @@ declare -A CHECKS=(
 	["set-user"]=""
 	["preserve-env"]="1"
 	["static-port-forwards"]=""
+	["ssh-over-vsock"]=""
 )
 
 case "$NAME" in
@@ -68,6 +69,7 @@ case "$NAME" in
 	# CI failure:
 	# "[hostagent] failed to confirm whether /c/Users/runneradmin [remote] is successfully mounted"
 	[ "${OS_HOST}" = "Msys" ] && CHECKS["mount-home"]=
+	[ "${OS_HOST}" = "Darwin" ] && CHECKS["ssh-over-vsock"]="1"
 	;;
 "alpine"*)
 	WARNING "Alpine does not support systemd"
@@ -341,6 +343,38 @@ if [[ -n ${CHECKS["preserve-env"]} ]]; then
 	"${scriptdir}"/test-preserve-env.sh "$NAME"
 fi
 
+if [[ -n ${CHECKS["ssh-over-vsock"]} ]]; then
+	if [[ "$(limactl ls "${NAME}" --yq .vmType)" == "vz" ]]; then
+		INFO "Testing SSH over vsock"
+		set -x
+		INFO "Testing LIMA_SSH_OVER_VSOCK=true environment"
+		limactl stop "${NAME}"
+		if ! LIMA_SSH_OVER_VSOCK=true limactl start "${NAME}" 2>&1 | grep -i "started vsock forwarder"; then
+			set +x
+			diagnose "${NAME}"
+			ERROR "LIMA_SSH_OVER_VSOCK=true did not enable vsock forwarder"
+			exit 1
+		fi
+		INFO 'Testing LIMA_SSH_OVER_VSOCK="" environment'
+		limactl stop "${NAME}"
+		if ! LIMA_SSH_OVER_VSOCK="" limactl start "${NAME}" 2>&1 | grep -i "started vsock forwarder"; then
+			set +x
+			diagnose "${NAME}"
+			ERROR "LIMA_SSH_OVER_VSOCK= did not enable vsock forwarder"
+			exit 1
+		fi
+		INFO "Testing LIMA_SSH_OVER_VSOCK=false environment"
+		limactl stop "${NAME}"
+		if ! LIMA_SSH_OVER_VSOCK=false limactl start "${NAME}" 2>&1 | grep -i "skipping detection of SSH server on vsock port"; then
+			set +x
+			diagnose "${NAME}"
+			ERROR "LIMA_SSH_OVER_VSOCK=false did not disable vsock forwarder"
+			exit 1
+		fi
+		set +x
+	fi
+fi
+
 # Use GHCR to avoid hitting Docker Hub rate limit
 nginx_image="ghcr.io/stargz-containers/nginx:1.19-alpine-org"
 alpine_image="ghcr.io/containerd/alpine:3.14.0"
@@ -348,7 +382,7 @@ alpine_image="ghcr.io/containerd/alpine:3.14.0"
 if [[ -n ${CHECKS["container-engine"]} ]]; then
 	sudo=""
 	# Currently WSL2 machines only support privileged engine. This requirement might be lifted in the future.
-	if [[ "$(limactl ls --json "${NAME}" | jq -r .vmType)" == "wsl2" ]]; then
+	if [[ "$(limactl ls "${NAME}" --yq .vmType)" == "wsl2" ]]; then
 		sudo="sudo"
 	fi
 	INFO "Run a nginx container with port forwarding 127.0.0.1:8080"
@@ -428,7 +462,7 @@ if [[ -n ${CHECKS["port-forwards"]} ]]; then
 				sudo="sudo"
 			fi
 			# Currently WSL2 machines only support privileged engine. This requirement might be lifted in the future.
-			if [[ "$(limactl ls --json "${NAME}" | jq -r .vmType)" == "wsl2" ]]; then
+			if [[ "$(limactl ls "${NAME}" --yq .vmType)" == "wsl2" ]]; then
 				sudo="sudo"
 			fi
 			limactl shell "$NAME" $sudo $CONTAINER_ENGINE info
@@ -613,7 +647,7 @@ if [[ -n ${CHECKS["clone"]} ]]; then
 	limactl start "$NAME"
 fi
 
-if [[ $NAME == "fedora" && "$(limactl ls --json "$NAME" | jq -r .vmType)" == "vz" ]]; then
+if [[ $NAME == "fedora" && "$(limactl ls "${NAME}" --yq .vmType)" == "vz" ]]; then
 	"${scriptdir}"/test-selinux.sh "$NAME"
 fi
 
diff --git a/hack/test-upgrade.sh b/hack/test-upgrade.sh
index e9be6c15..df762e27 100755
--- a/hack/test-upgrade.sh
+++ b/hack/test-upgrade.sh
@@ -24,7 +24,7 @@ function install_lima() {
 	git checkout "${ver}"
 	make clean
 	make
-	if [ -w "${PREFIX}/bin" ] && [ -w "${PREFIX}/share" ]; then
+	if [ -w "${PREFIX}/bin" ] && [ -w "${PREFIX}/share" ] && [ -w "${PREFIX}/libexec" ]; then
 		make install
 	else
 		sudo make install
@@ -34,15 +34,15 @@ function install_lima() {
 function install_lima_binary() {
 	ver="$1"
 	tar="tar"
-	if [ ! -w "${PREFIX}/bin" ] || [ ! -w "${PREFIX}/share" ]; then
+	if [ ! -w "${PREFIX}/bin" ] || [ ! -w "${PREFIX}/share" ] || [ ! -w "${PREFIX}/libexec" ]; then
 		tar="sudo ${tar}"
 	fi
 	curl -fsSL "https://github.com/lima-vm/lima/releases/download/${ver}/lima-${ver:1}-$(uname -s)-$(uname -m).tar.gz" | ${tar} Cxzvm "${PREFIX}"
 }
 
 function uninstall_lima() {
-	files="${PREFIX}/bin/lima ${PREFIX}/bin/limactl ${PREFIX}/share/lima ${PREFIX}/share/doc/lima"
-	if [ -w "${PREFIX}/bin" ] && [ -w "${PREFIX}/share" ]; then
+	files="${PREFIX}/bin/lima ${PREFIX}/bin/limactl ${PREFIX}/share/lima ${PREFIX}/share/doc/lima ${PREFIX}/libexec/lima"
+	if [ -w "${PREFIX}/bin" ] && [ -w "${PREFIX}/share" ] && [ -w "${PREFIX}/libexec" ]; then
 		# shellcheck disable=SC2086
 		rm -rf $files
 	else
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
index 302ff6ca..fa207c13 100644
--- a/hack/tools/go.mod
+++ b/hack/tools/go.mod
@@ -14,7 +14,7 @@ tool (
 
 require (
 	github.com/containerd/ltag v0.3.0
-	github.com/golangci/golangci-lint/v2 v2.4.0
+	github.com/golangci/golangci-lint/v2 v2.5.0
 	github.com/yoheimuta/protolint v0.56.4
 	google.golang.org/grpc v1.75.1
 	google.golang.org/protobuf v1.36.8
@@ -29,13 +29,15 @@ require (
 	dev.gaijin.team/go/golib v0.6.0 // indirect
 	github.com/4meepo/tagalign v1.4.3 // indirect
 	github.com/Abirdcfly/dupword v0.1.6 // indirect
+	github.com/AdminBenni/iota-mixing v1.0.0 // indirect
 	github.com/AlwxSin/noinlineerr v1.0.5 // indirect
-	github.com/Antonboom/errname v1.1.0 // indirect
-	github.com/Antonboom/nilnil v1.1.0 // indirect
-	github.com/Antonboom/testifylint v1.6.1 // indirect
+	github.com/Antonboom/errname v1.1.1 // indirect
+	github.com/Antonboom/nilnil v1.1.1 // indirect
+	github.com/Antonboom/testifylint v1.6.4 // indirect
 	github.com/BurntSushi/toml v1.5.0 // indirect
-	github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
+	github.com/Djarvur/go-err113 v0.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.3.1 // indirect
+	github.com/MirrexOne/unqueryvet v1.2.1 // indirect
 	github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
 	github.com/alecthomas/chroma/v2 v2.20.0 // indirect
 	github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
@@ -51,7 +53,7 @@ require (
 	github.com/bkielbasa/cyclop v1.2.3 // indirect
 	github.com/blizzy78/varnamelen v0.8.0 // indirect
 	github.com/bombsimon/wsl/v4 v4.7.0 // indirect
-	github.com/bombsimon/wsl/v5 v5.1.1 // indirect
+	github.com/bombsimon/wsl/v5 v5.2.0 // indirect
 	github.com/breml/bidichk v0.3.3 // indirect
 	github.com/breml/errchkjson v0.4.1 // indirect
 	github.com/butuzov/ireturn v0.4.0 // indirect
@@ -80,7 +82,7 @@ require (
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/fzipp/gocyclo v0.6.0 // indirect
 	github.com/gertd/go-pluralize v0.2.1 // indirect
-	github.com/ghostiam/protogetter v0.3.15 // indirect
+	github.com/ghostiam/protogetter v0.3.16 // indirect
 	github.com/go-critic/go-critic v0.13.0 // indirect
 	github.com/go-toolsmith/astcast v1.1.0 // indirect
 	github.com/go-toolsmith/astcopy v1.1.0 // indirect
@@ -92,24 +94,26 @@ require (
 	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
 	github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
 	github.com/gobwas/glob v0.2.3 // indirect
+	github.com/godoc-lint/godoc-lint v0.10.0 // indirect
 	github.com/gofrs/flock v0.12.1 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
+	github.com/golangci/asciicheck v0.5.0 // indirect
 	github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
-	github.com/golangci/go-printf-func-name v0.1.0 // indirect
+	github.com/golangci/go-printf-func-name v0.1.1 // indirect
 	github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
 	github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect
 	github.com/golangci/misspell v0.7.0 // indirect
+	github.com/golangci/nilerr v0.0.0-20250918000102-015671e622fe // indirect
 	github.com/golangci/plugin-module-register v0.1.2 // indirect
 	github.com/golangci/revgrep v0.8.0 // indirect
 	github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
 	github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/renameio/v2 v2.0.0 // indirect
-	github.com/gordonklaus/ineffassign v0.1.0 // indirect
+	github.com/gordonklaus/ineffassign v0.2.0 // indirect
 	github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
 	github.com/gostaticanalysis/comment v1.5.0 // indirect
 	github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
-	github.com/gostaticanalysis/nilerr v0.1.1 // indirect
 	github.com/hashicorp/go-hclog v1.6.3 // indirect
 	github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
 	github.com/hashicorp/go-plugin v1.6.3 // indirect
@@ -126,19 +130,19 @@ require (
 	github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
 	github.com/kisielk/errcheck v1.9.0 // indirect
 	github.com/kkHAIKE/contextcheck v1.1.6 // indirect
-	github.com/kulti/thelper v0.6.3 // indirect
+	github.com/kulti/thelper v0.7.1 // indirect
 	github.com/kunwardeep/paralleltest v1.0.14 // indirect
 	github.com/lasiar/canonicalheader v1.1.2 // indirect
 	github.com/ldez/exptostd v0.4.4 // indirect
 	github.com/ldez/gomoddirectives v0.7.0 // indirect
-	github.com/ldez/grignotin v0.10.0 // indirect
-	github.com/ldez/tagliatelle v0.7.1 // indirect
+	github.com/ldez/grignotin v0.10.1 // indirect
+	github.com/ldez/tagliatelle v0.7.2 // indirect
 	github.com/ldez/usetesting v0.5.0 // indirect
 	github.com/leonklingele/grouper v1.1.2 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/macabu/inamedparam v0.2.0 // indirect
 	github.com/magiconair/properties v1.8.6 // indirect
-	github.com/manuelarte/embeddedstructfieldcheck v0.3.0 // indirect
+	github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
 	github.com/manuelarte/funcorder v0.5.0 // indirect
 	github.com/maratori/testableexamples v1.0.0 // indirect
 	github.com/maratori/testpackage v1.1.1 // indirect
@@ -147,7 +151,7 @@ require (
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
-	github.com/mgechev/revive v1.11.0 // indirect
+	github.com/mgechev/revive v1.12.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/moricho/tparallel v0.3.2 // indirect
@@ -155,7 +159,7 @@ require (
 	github.com/nakabonne/nestif v0.3.1 // indirect
 	github.com/nishanths/exhaustive v0.12.0 // indirect
 	github.com/nishanths/predeclared v0.2.2 // indirect
-	github.com/nunnatsa/ginkgolinter v0.20.0 // indirect
+	github.com/nunnatsa/ginkgolinter v0.21.0 // indirect
 	github.com/oklog/run v1.2.0 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
@@ -179,24 +183,23 @@ require (
 	github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
 	github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
 	github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
-	github.com/securego/gosec/v2 v2.22.7 // indirect
+	github.com/securego/gosec/v2 v2.22.8 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/sivchari/containedctx v1.0.3 // indirect
 	github.com/sonatard/noctx v0.4.0 // indirect
 	github.com/sourcegraph/go-diff v0.7.0 // indirect
 	github.com/spf13/afero v1.14.0 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
-	github.com/spf13/cobra v1.9.1 // indirect
+	github.com/spf13/cobra v1.10.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
-	github.com/spf13/pflag v1.0.7 // indirect
+	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/spf13/viper v1.12.0 // indirect
 	github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
 	github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
 	github.com/stretchr/objx v0.5.2 // indirect
-	github.com/stretchr/testify v1.10.0 // indirect
+	github.com/stretchr/testify v1.11.1 // indirect
 	github.com/subosito/gotenv v1.4.1 // indirect
-	github.com/tdakkota/asciicheck v0.4.1 // indirect
-	github.com/tetafro/godot v1.5.1 // indirect
+	github.com/tetafro/godot v1.5.4 // indirect
 	github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
 	github.com/timonwong/loggercheck v0.11.0 // indirect
 	github.com/tomarrell/wrapcheck/v2 v2.11.0 // indirect
@@ -212,28 +215,28 @@ require (
 	github.com/ykadowak/zerologlint v0.1.5 // indirect
 	github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect
 	gitlab.com/bosi/decorder v0.4.2 // indirect
-	go-simpler.org/musttag v0.13.1 // indirect
+	go-simpler.org/musttag v0.14.0 // indirect
 	go-simpler.org/sloglint v0.11.1 // indirect
 	go.augendre.info/arangolint v0.2.0 // indirect
-	go.augendre.info/fatcontext v0.8.0 // indirect
+	go.augendre.info/fatcontext v0.8.1 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
 	go.uber.org/multierr v1.10.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
-	golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect
-	golang.org/x/mod v0.27.0 // indirect
+	golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621 // indirect
+	golang.org/x/mod v0.28.0 // indirect
 	golang.org/x/net v0.44.0 // indirect
 	golang.org/x/sync v0.17.0 // indirect
 	golang.org/x/sys v0.36.0 // indirect
 	golang.org/x/term v0.35.0 // indirect
 	golang.org/x/text v0.29.0 // indirect
-	golang.org/x/tools v0.36.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect
+	golang.org/x/tools v0.37.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	honnef.co/go/tools v0.6.1 // indirect
 	mvdan.cc/editorconfig v0.3.0 // indirect
-	mvdan.cc/gofumpt v0.8.0 // indirect
+	mvdan.cc/gofumpt v0.9.1 // indirect
 	mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 // indirect
 )
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
index 5ec77e23..abcc4db5 100644
--- a/hack/tools/go.sum
+++ b/hack/tools/go.sum
@@ -45,22 +45,26 @@ github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8
 github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
 github.com/Abirdcfly/dupword v0.1.6 h1:qeL6u0442RPRe3mcaLcbaCi2/Y/hOcdtw6DE9odjz9c=
 github.com/Abirdcfly/dupword v0.1.6/go.mod h1:s+BFMuL/I4YSiFv29snqyjwzDp4b65W2Kvy+PKzZ6cw=
+github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
+github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
 github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY=
 github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
-github.com/Antonboom/errname v1.1.0 h1:A+ucvdpMwlo/myWrkHEUEBWc/xuXdud23S8tmTb/oAE=
-github.com/Antonboom/errname v1.1.0/go.mod h1:O1NMrzgUcVBGIfi3xlVuvX8Q/VP/73sseCaAppfjqZw=
-github.com/Antonboom/nilnil v1.1.0 h1:jGxJxjgYS3VUUtOTNk8Z1icwT5ESpLH/426fjmQG+ng=
-github.com/Antonboom/nilnil v1.1.0/go.mod h1:b7sAlogQjFa1wV8jUW3o4PMzDVFLbTux+xnQdvzdcIE=
-github.com/Antonboom/testifylint v1.6.1 h1:6ZSytkFWatT8mwZlmRCHkWz1gPi+q6UBSbieji2Gj/o=
-github.com/Antonboom/testifylint v1.6.1/go.mod h1:k+nEkathI2NFjKO6HvwmSrbzUcQ6FAnbZV+ZRrnXPLI=
+github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q=
+github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ=
+github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ=
+github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II=
+github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
+github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
 github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=
-github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
+github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
+github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=
 github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
 github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/MirrexOne/unqueryvet v1.2.1 h1:M+zdXMq84g+E1YOLa7g7ExN3dWfZQrdDSTCM7gC+m/A=
+github.com/MirrexOne/unqueryvet v1.2.1/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg=
 github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
 github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
 github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
@@ -102,8 +106,8 @@ github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ
 github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
 github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=
 github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
-github.com/bombsimon/wsl/v5 v5.1.1 h1:cQg5KJf9FlctAH4cpL9vLKnziYknoCMCdqXl0wjl72Q=
-github.com/bombsimon/wsl/v5 v5.1.1/go.mod h1:Gp8lD04z27wm3FANIUPZycXp+8huVsn0oxc+n4qfV9I=
+github.com/bombsimon/wsl/v5 v5.2.0 h1:PyCCwd3Q7abGs3e34IW4jLYlBS+FbsU6iK+Tb3NnDp4=
+github.com/bombsimon/wsl/v5 v5.2.0/go.mod h1:Gp8lD04z27wm3FANIUPZycXp+8huVsn0oxc+n4qfV9I=
 github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE=
 github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE=
 github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg=
@@ -183,8 +187,8 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
 github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
 github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=
 github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk=
-github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY=
-github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
+github.com/ghostiam/protogetter v0.3.16 h1:UkrisuJBYLnZW6FcYUNBDJOqY3X22RtoYMlCsiNlFFA=
+github.com/ghostiam/protogetter v0.3.16/go.mod h1:4SRRIv6PcjkIMpUkRUsP4TsUTqO/N3Fmvwivuc/sCHA=
 github.com/go-critic/go-critic v0.13.0 h1:kJzM7wzltQasSUXtYyTl6UaPVySO6GkaR1thFnJ6afY=
 github.com/go-critic/go-critic v0.13.0/go.mod h1:M/YeuJ3vOCQDnP2SU+ZhjgRzwzcBW87JqLpMJLrZDLI=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -230,6 +234,8 @@ github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUW
 github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/godoc-lint/godoc-lint v0.10.0 h1:OcyrziBi18sQSEpib6NesVHEJ/Xcng97NunePBA48g4=
+github.com/godoc-lint/godoc-lint v0.10.0/go.mod h1:KleLcHu/CGSvkjUH2RvZyoK1MBC7pDQg4NxMYLcBBsw=
 github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
 github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -262,18 +268,22 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0=
+github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=
 github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw=
 github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
-github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU=
-github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s=
+github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=
+github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
 github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
 github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
-github.com/golangci/golangci-lint/v2 v2.4.0 h1:qz6O6vr7kVzXJqyvHjHSz5fA3D+PM8v96QU5gxZCNWM=
-github.com/golangci/golangci-lint/v2 v2.4.0/go.mod h1:Oq7vuAf6L1iNL34uHDcsIF6Mnc0amOPdsT3/GlpHD+I=
+github.com/golangci/golangci-lint/v2 v2.5.0 h1:BDRg4ASm4J1y/DSRY6zwJ5tr5Yy8ZqbZ79XrCeFxaQo=
+github.com/golangci/golangci-lint/v2 v2.5.0/go.mod h1:IJtWJBZkLbx7AVrIUzLd8Oi3ADtwaNpWbR3wthVWHcc=
 github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8=
 github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95/go.mod h1:k9mmcyWKSTMcPPvQUCfRWWQ9VHJ1U9Dc0R7kaXAgtnQ=
 github.com/golangci/misspell v0.7.0 h1:4GOHr/T1lTW0hhR4tgaaV1WS/lJ+ncvYCoFKmqJsj0c=
 github.com/golangci/misspell v0.7.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=
+github.com/golangci/nilerr v0.0.0-20250918000102-015671e622fe h1:F1pK9tBy41i7eesBFkSNMldwtiAaWiU+3fT/24sTnNI=
+github.com/golangci/nilerr v0.0.0-20250918000102-015671e622fe/go.mod h1:CtTxAluxD2ng9aIT9bPrVoMuISFWCD+SaxtvYtdWA2k=
 github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg=
 github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=
 github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
@@ -316,18 +326,15 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
-github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
+github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
+github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
 github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
 github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
-github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
 github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
 github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=
 github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=
 github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=
 github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=
-github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk=
-github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A=
 github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
 github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=
 github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
@@ -391,8 +398,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs=
-github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I=
+github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98=
+github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs=
 github.com/kunwardeep/paralleltest v1.0.14 h1:wAkMoMeGX/kGfhQBPODT/BL8XhK23ol/nuQ3SwFaUw8=
 github.com/kunwardeep/paralleltest v1.0.14/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=
 github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4=
@@ -401,10 +408,10 @@ github.com/ldez/exptostd v0.4.4 h1:58AtQjnLcT/tI5W/1KU7xE/O7zW9RAWB6c/ScQAnfus=
 github.com/ldez/exptostd v0.4.4/go.mod h1:QfdzPw6oHjFVdNV7ILoPu5sw3OZ3OG1JS0I5JN3J4Js=
 github.com/ldez/gomoddirectives v0.7.0 h1:EOx8Dd56BZYSez11LVgdj025lKwlP0/E5OLSl9HDwsY=
 github.com/ldez/gomoddirectives v0.7.0/go.mod h1:wR4v8MN9J8kcwvrkzrx6sC9xe9Cp68gWYCsda5xvyGc=
-github.com/ldez/grignotin v0.10.0 h1:NQPeh1E/Eza4F0exCeC1WkpnLvgUcQDT8MQ1vOLML0E=
-github.com/ldez/grignotin v0.10.0/go.mod h1:oR4iCKUP9fwoeO6vCQeD7M5SMxCT6xdVas4vg0h1LaI=
-github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk=
-github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I=
+github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
+github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=
+github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk=
+github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI=
 github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=
 github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ=
 github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
@@ -415,8 +422,8 @@ github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddB
 github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=
 github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
 github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/manuelarte/embeddedstructfieldcheck v0.3.0 h1:VhGqK8gANDvFYDxQkjPbv7/gDJtsGU9k6qj/hC2hgso=
-github.com/manuelarte/embeddedstructfieldcheck v0.3.0/go.mod h1:LSo/IQpPfx1dXMcX4ibZCYA7Yy6ayZHIaOGM70+1Wy8=
+github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww=
+github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM=
 github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8=
 github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA=
 github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI=
@@ -439,8 +446,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/mgechev/revive v1.11.0 h1:b/gLLpBE427o+Xmd8G58gSA+KtBwxWinH/A565Awh0w=
-github.com/mgechev/revive v1.11.0/go.mod h1:tI0oLF/2uj+InHCBLrrqfTKfjtFTBCFFfG05auyzgdw=
+github.com/mgechev/revive v1.12.0 h1:Q+/kkbbwerrVYPv9d9efaPGmAO/NsxwW/nE6ahpQaCU=
+github.com/mgechev/revive v1.12.0/go.mod h1:VXsY2LsTigk8XU9BpZauVLjVrhICMOV3k1lpB3CXrp8=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -462,14 +469,14 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK
 github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
 github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
 github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
-github.com/nunnatsa/ginkgolinter v0.20.0 h1:OmWLkAFO2HUTYcU6mprnKud1Ey5pVdiVNYGO5HVicx8=
-github.com/nunnatsa/ginkgolinter v0.20.0/go.mod h1:dCIuFlTPfQerXgGUju3VygfAFPdC5aE1mdacCDKDJcQ=
+github.com/nunnatsa/ginkgolinter v0.21.0 h1:IYwuX+ajy3G1MezlMLB1BENRtFj16+Evyi4uki1NOOQ=
+github.com/nunnatsa/ginkgolinter v0.21.0/go.mod h1:QlzY9UP9zaqu58FjYxhp9bnjuwXwG1bfW5rid9ChNMw=
 github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
 github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
 github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
 github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
-github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
-github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
+github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY=
+github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o=
 github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
 github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
 github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
@@ -543,8 +550,8 @@ github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tM
 github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
 github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
 github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
-github.com/securego/gosec/v2 v2.22.7 h1:8/9P+oTYI4yIpAzccQKVsg1/90Po+JzGtAhqoHImDeM=
-github.com/securego/gosec/v2 v2.22.7/go.mod h1:510TFNDMrIPytokyHQAVLvPeDr41Yihn2ak8P+XQfNE=
+github.com/securego/gosec/v2 v2.22.8 h1:3NMpmfXO8wAVFZPNsd3EscOTa32Jyo6FLLlW53bexMI=
+github.com/securego/gosec/v2 v2.22.8/go.mod h1:ZAw8K2ikuH9qDlfdV87JmNghnVfKB1XC7+TVzk6Utto=
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
@@ -564,14 +571,14 @@ github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
 github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
-github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
-github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
+github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
-github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
 github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
 github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
@@ -592,18 +599,16 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
 github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
-github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=
-github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8=
 github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
 github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
 github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
 github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
-github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4=
-github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk=
+github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg=
+github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
 github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk=
 github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
 github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M=
@@ -645,14 +650,14 @@ gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
 gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
 go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=
 go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28=
-go-simpler.org/musttag v0.13.1 h1:lw2sJyu7S1X8lc8zWUAdH42y+afdcCnHhWpnkWvd6vU=
-go-simpler.org/musttag v0.13.1/go.mod h1:8r450ehpMLQgvpb6sg+hV5Ur47eH6olp/3yEanfG97k=
+go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=
+go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=
 go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s=
 go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ=
 go.augendre.info/arangolint v0.2.0 h1:2NP/XudpPmfBhQKX4rMk+zDYIj//qbt4hfZmSSTcpj8=
 go.augendre.info/arangolint v0.2.0/go.mod h1:Vx4KSJwu48tkE+8uxuf0cbBnAPgnt8O1KWiT7bljq7w=
-go.augendre.info/fatcontext v0.8.0 h1:2dfk6CQbDGeu1YocF59Za5Pia7ULeAM6friJ3LP7lmk=
-go.augendre.info/fatcontext v0.8.0/go.mod h1:oVJfMgwngMsHO+KB2MdgzcO+RvtNdiCEOlWvSFtax/s=
+go.augendre.info/fatcontext v0.8.1 h1:/T4+cCjpL9g71gJpcFAgVo/K5VFpqlN+NPU7QXxD5+A=
+go.augendre.info/fatcontext v0.8.1/go.mod h1:r3Qz4ZOzex66wfyyj5VZ1xUcl81vzvHQ6/GWzzlMEwA=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -701,8 +706,8 @@ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWB
 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b h1:KdrhdYPDUvJTvrDK9gdjfFd6JTk8vA1WJoldYSi0kHo=
-golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
+golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621 h1:Yl4H5w2RV7L/dvSHp2GerziT5K2CORgFINPaMFxWGWw=
+golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -730,8 +735,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
-golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
+golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
+golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -907,7 +912,6 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
 golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
 golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
 golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
 golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -917,20 +921,17 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
 golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 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.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
-golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
-golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
+golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
+golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
 golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
 golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
 golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
@@ -992,8 +993,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
 google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 h1:qJW29YvkiJmXOYMu5Tf8lyrTp3dOS+K4z6IixtLaCf8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1054,8 +1055,8 @@ honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
 honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
 mvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ=
 mvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ=
-mvdan.cc/gofumpt v0.8.0 h1:nZUCeC2ViFaerTcYKstMmfysj6uhQrA2vJe+2vwGU6k=
-mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg=
+mvdan.cc/gofumpt v0.9.1 h1:p5YT2NfFWsYyTieYgwcQ8aKV3xRvFH4uuN/zB2gBbMQ=
+mvdan.cc/gofumpt v0.9.1/go.mod h1:3xYtNemnKiXaTh6R4VtlqDATFwBbdXI8lJvH/4qk7mw=
 mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
 mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=
 mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 h1:WjUu4yQoT5BHT1w8Zu56SP8367OuBV5jvo+4Ulppyf8=
diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go
index ad024acb..888e17c5 100644
--- a/pkg/cidata/cidata.go
+++ b/pkg/cidata/cidata.go
@@ -118,7 +118,7 @@ func setupEnv(instConfigEnv map[string]string, propagateProxyEnv bool, slirpGate
 	return env, nil
 }
 
-func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string, noCloudInit bool) (*TemplateArgs, error) {
+func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string, noCloudInit, rosettaEnabled, rosettaBinFmt bool) (*TemplateArgs, error) {
 	if err := limayaml.Validate(instConfig, false); err != nil {
 		return nil, err
 	}
@@ -138,20 +138,15 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i
 		Containerd:         Containerd{System: *instConfig.Containerd.System, User: *instConfig.Containerd.User, Archive: archive},
 		SlirpNICName:       networks.SlirpNICName,
 
-		VMType:      *instConfig.VMType,
-		VSockPort:   vsockPort,
-		VirtioPort:  virtioPort,
-		Plain:       *instConfig.Plain,
-		TimeZone:    *instConfig.TimeZone,
-		NoCloudInit: noCloudInit,
-		Param:       instConfig.Param,
-	}
-
-	if instConfig.VMOpts.VZ.Rosetta.Enabled != nil {
-		args.RosettaEnabled = *instConfig.VMOpts.VZ.Rosetta.Enabled
-	}
-	if instConfig.VMOpts.VZ.Rosetta.BinFmt != nil {
-		args.RosettaBinFmt = *instConfig.VMOpts.VZ.Rosetta.BinFmt
+		VMType:         *instConfig.VMType,
+		VSockPort:      vsockPort,
+		VirtioPort:     virtioPort,
+		RosettaEnabled: rosettaEnabled,
+		RosettaBinFmt:  rosettaBinFmt,
+		Plain:          *instConfig.Plain,
+		TimeZone:       *instConfig.TimeZone,
+		NoCloudInit:    noCloudInit,
+		Param:          instConfig.Param,
 	}
 
 	firstUsernetIndex := limayaml.FirstUsernetIndex(instConfig)
@@ -351,7 +346,7 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i
 }
 
 func GenerateCloudConfig(ctx context.Context, instDir, name string, instConfig *limatype.LimaYAML) error {
-	args, err := templateArgs(ctx, false, instDir, name, instConfig, 0, 0, 0, "", false)
+	args, err := templateArgs(ctx, false, instDir, name, instConfig, 0, 0, 0, "", false, false, false)
 	if err != nil {
 		return err
 	}
@@ -373,8 +368,8 @@ func GenerateCloudConfig(ctx context.Context, instDir, name string, instConfig *
 	return os.WriteFile(filepath.Join(instDir, filenames.CloudConfig), config, 0o444)
 }
 
-func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, guestAgentBinary, nerdctlArchive string, vsockPort int, virtioPort string, noCloudInit bool) error {
-	args, err := templateArgs(ctx, true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort, noCloudInit)
+func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, guestAgentBinary, nerdctlArchive string, vsockPort int, virtioPort string, noCloudInit, rosettaEnabled, rosettaBinFmt bool) error {
+	args, err := templateArgs(ctx, true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort, noCloudInit, rosettaEnabled, rosettaBinFmt)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go
index fc3a1afd..199119e7 100644
--- a/pkg/driver/driver.go
+++ b/pkg/driver/driver.go
@@ -107,4 +107,6 @@ type DriverFeatures struct {
 	DynamicSSHAddress    bool `json:"dynamicSSHAddress"`
 	SkipSocketForwarding bool `json:"skipSocketForwarding"`
 	NoCloudInit          bool `json:"noCloudInit"`
+	RosettaEnabled       bool `json:"rosettaEnabled"`
+	RosettaBinFmt        bool `json:"rosettaBinFmt"`
 }
diff --git a/pkg/driver/vz/vm_darwin.go b/pkg/driver/vz/vm_darwin.go
index e39c5493..8d1a0577 100644
--- a/pkg/driver/vz/vm_darwin.go
+++ b/pkg/driver/vz/vm_darwin.go
@@ -72,12 +72,8 @@ func startVM(ctx context.Context, inst *limatype.Instance, sshLocalPort int) (*v
 
 	errCh := make(chan error)
 
-	filesToRemove := make(map[string]struct{})
-	defer func() {
-		for f := range filesToRemove {
-			_ = os.RemoveAll(f)
-		}
-	}()
+	waitSSHLocalPortAccessible := make(chan struct{})
+	defer close(waitSSHLocalPortAccessible)
 	go func() {
 		// Handle errors via errCh and handle stop vm during context close
 		defer func() {
@@ -105,13 +101,36 @@ func startVM(ctx context.Context, inst *limatype.Instance, sshLocalPort int) (*v
 						logrus.Errorf("error writing to pid fil %q", pidFile)
 						errCh <- err
 					}
-					filesToRemove[pidFile] = struct{}{}
 					logrus.Info("[VZ] - vm state change: running")
 
-					err := usernetClient.ConfigureDriver(ctx, inst, sshLocalPort)
+					usernetSSHLocalPort := sshLocalPort
+					useSSHOverVsock := true
+					if envVar := os.Getenv("LIMA_SSH_OVER_VSOCK"); envVar != "" {
+						b, err := strconv.ParseBool(envVar)
+						if err != nil {
+							logrus.WithError(err).Warnf("invalid LIMA_SSH_OVER_VSOCK value %q", envVar)
+						} else {
+							useSSHOverVsock = b
+						}
+					}
+					if !useSSHOverVsock {
+						logrus.Info("LIMA_SSH_OVER_VSOCK is false, skipping detection of SSH server on vsock port")
+					} else if err := usernetClient.WaitOpeningSSHPort(ctx, inst); err == nil {
+						hostAddress := net.JoinHostPort(inst.SSHAddress, strconv.Itoa(usernetSSHLocalPort))
+						if err := wrapper.startVsockForwarder(ctx, 22, hostAddress); err == nil {
+							logrus.Infof("Detected SSH server is listening on the vsock port; changed %s to proxy for the vsock port", hostAddress)
+							usernetSSHLocalPort = 0 // disable gvisor ssh port forwarding
+						} else {
+							logrus.WithError(err).Warn("Failed to detect SSH server on vsock port, falling back to usernet forwarder")
+						}
+					} else {
+						logrus.WithError(err).Warn("Failed to wait for the guest SSH server to become available, falling back to usernet forwarder")
+					}
+					err := usernetClient.ConfigureDriver(ctx, inst, usernetSSHLocalPort)
 					if err != nil {
 						errCh <- err
 					}
+					waitSSHLocalPortAccessible <- struct{}{}
 				case vz.VirtualMachineStateStopped:
 					logrus.Info("[VZ] - vm state change: stopped")
 					wrapper.mu.Lock()
@@ -128,7 +147,7 @@ func startVM(ctx context.Context, inst *limatype.Instance, sshLocalPort int) (*v
 			}
 		}
 	}()
-
+	<-waitSSHLocalPortAccessible
 	return wrapper, errCh, err
 }
 
diff --git a/pkg/driver/vz/vsock_forwarder.go b/pkg/driver/vz/vsock_forwarder.go
new file mode 100644
index 00000000..044c3d51
--- /dev/null
+++ b/pkg/driver/vz/vsock_forwarder.go
@@ -0,0 +1,75 @@
+//go:build darwin && !no_vz
+
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package vz
+
+import (
+	"context"
+	"errors"
+	"net"
+
+	"github.com/containers/gvisor-tap-vsock/pkg/tcpproxy"
+	"github.com/sirupsen/logrus"
+)
+
+func (m *virtualMachineWrapper) startVsockForwarder(ctx context.Context, vsockPort uint32, hostAddress string) error {
+	// Test if the vsock port is open
+	conn, err := m.dialVsock(ctx, vsockPort)
+	if err != nil {
+		return err
+	}
+	conn.Close()
+	// Start listening on localhost:hostPort and forward to vsock:vsockPort
+	_, _, err = net.SplitHostPort(hostAddress)
+	if err != nil {
+		return err
+	}
+	var lc net.ListenConfig
+	l, err := lc.Listen(ctx, "tcp", hostAddress)
+	if err != nil {
+		return err
+	}
+	go func() {
+		<-ctx.Done()
+		l.Close()
+	}()
+	logrus.Infof("Started vsock forwarder: %s -> vsock:%d on VM", hostAddress, vsockPort)
+	go func() {
+		defer l.Close()
+		for {
+			conn, err := l.Accept()
+			if err != nil {
+				if errors.Is(err, net.ErrClosed) {
+					return
+				}
+				logrus.WithError(err).Errorf("vsock forwarder accept error: %v", err)
+			} else {
+				p := tcpproxy.DialProxy{
+					DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
+						return m.dialVsock(ctx, vsockPort)
+					},
+				}
+				go p.HandleConn(conn)
+			}
+			select {
+			case <-ctx.Done():
+				return
+			default:
+				continue
+			}
+		}
+	}()
+	return nil
+}
+
+func (m *virtualMachineWrapper) dialVsock(_ context.Context, port uint32) (conn net.Conn, err error) {
+	for _, socket := range m.SocketDevices() {
+		conn, err = socket.Connect(port)
+		if err == nil {
+			return conn, nil
+		}
+	}
+	return nil, err
+}
diff --git a/pkg/driver/vz/vz_driver_darwin.go b/pkg/driver/vz/vz_driver_darwin.go
index 235557e5..ec901b73 100644
--- a/pkg/driver/vz/vz_driver_darwin.go
+++ b/pkg/driver/vz/vz_driver_darwin.go
@@ -74,9 +74,11 @@ const Enabled = true
 type LimaVzDriver struct {
 	Instance *limatype.Instance
 
-	SSHLocalPort int
-	vSockPort    int
-	virtioPort   string
+	SSHLocalPort   int
+	vSockPort      int
+	virtioPort     string
+	rosettaEnabled bool
+	rosettaBinFmt  bool
 
 	machine *virtualMachineWrapper
 }
@@ -106,6 +108,15 @@ func (l *LimaVzDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDriv
 		}
 	}
 
+	if runtime.GOOS == "darwin" && limayaml.IsNativeArch(limatype.AARCH64) {
+		if l.Instance.Config.VMOpts.VZ.Rosetta.Enabled != nil {
+			l.rosettaEnabled = *l.Instance.Config.VMOpts.VZ.Rosetta.Enabled
+		}
+	}
+	if l.Instance.Config.VMOpts.VZ.Rosetta.BinFmt != nil {
+		l.rosettaBinFmt = *l.Instance.Config.VMOpts.VZ.Rosetta.BinFmt
+	}
+
 	return &driver.ConfiguredDriver{
 		Driver: l,
 	}
@@ -351,6 +362,8 @@ func (l *LimaVzDriver) Info() driver.Info {
 		DynamicSSHAddress:    false,
 		SkipSocketForwarding: false,
 		CanRunGUI:            guiFlag,
+		RosettaEnabled:       l.rosettaEnabled,
+		RosettaBinFmt:        l.rosettaBinFmt,
 	}
 	return info
 }
diff --git a/pkg/guestagent/guestagent_linux.go b/pkg/guestagent/guestagent_linux.go
index 5b199131..534bd558 100644
--- a/pkg/guestagent/guestagent_linux.go
+++ b/pkg/guestagent/guestagent_linux.go
@@ -21,12 +21,13 @@ import (
 	"github.com/lima-vm/lima/v2/pkg/guestagent/iptables"
 	"github.com/lima-vm/lima/v2/pkg/guestagent/kubernetesservice"
 	"github.com/lima-vm/lima/v2/pkg/guestagent/procnettcp"
+	"github.com/lima-vm/lima/v2/pkg/guestagent/ticker"
 	"github.com/lima-vm/lima/v2/pkg/guestagent/timesync"
 )
 
-func New(ctx context.Context, newTicker func() (<-chan time.Time, func()), iptablesIdle time.Duration) (Agent, error) {
+func New(ctx context.Context, ticker ticker.Ticker, iptablesIdle time.Duration) (Agent, error) {
 	a := &agent{
-		newTicker:                newTicker,
+		ticker:                   ticker,
 		kubernetesServiceWatcher: kubernetesservice.NewServiceWatcher(),
 	}
 
@@ -67,11 +68,11 @@ func New(ctx context.Context, newTicker func() (<-chan time.Time, func()), iptab
 				return nil, err
 			}
 		}
-
-		go a.setWorthCheckingIPTablesRoutine(auditClient, iptablesIdle)
-	} else {
-		a.worthCheckingIPTables = true
 	}
+
+	a.worthCheckingIPTables = true // allow initial iptables scan
+	go a.setWorthCheckingIPTablesRoutine(auditClient, iptablesIdle)
+
 	logrus.Infof("Auditing enabled (%d)", auditStatus.Enabled)
 	return startGuestAgentRoutines(ctx, a, true), nil
 }
@@ -95,7 +96,7 @@ type agent struct {
 	// Ticker is like time.Ticker.
 	// We can't use inotify for /proc/net/tcp, so we need this ticker to
 	// reload /proc/net/tcp.
-	newTicker func() (<-chan time.Time, func())
+	ticker ticker.Ticker
 
 	worthCheckingIPTables    bool
 	worthCheckingIPTablesMu  sync.RWMutex
@@ -111,7 +112,8 @@ type agent struct {
 // when no NETFILTER_CFG audit message was received for the iptablesIdle time.
 func (a *agent) setWorthCheckingIPTablesRoutine(auditClient *libaudit.AuditClient, iptablesIdle time.Duration) {
 	logrus.Info("setWorthCheckingIPTablesRoutine(): monitoring netfilter audit events")
-	var latestTrue time.Time
+	// Initialize to now so the first sleeper loop does not immediately mark it false.
+	latestTrue := time.Now()
 	go func() {
 		for {
 			time.Sleep(iptablesIdle)
@@ -197,8 +199,8 @@ func isEventEmpty(ev *api.Event) bool {
 
 func (a *agent) Events(ctx context.Context, ch chan *api.Event) {
 	defer close(ch)
-	tickerCh, tickerClose := a.newTicker()
-	defer tickerClose()
+	tickerCh := a.ticker.Chan()
+	defer a.ticker.Stop()
 	var st eventState
 	for {
 		var ev *api.Event
diff --git a/pkg/guestagent/ticker/compound.go b/pkg/guestagent/ticker/compound.go
new file mode 100644
index 00000000..5373a562
--- /dev/null
+++ b/pkg/guestagent/ticker/compound.go
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package ticker
+
+import (
+	"time"
+)
+
+func NewCompoundTicker(t1, t2 Ticker) Ticker {
+	return &compoundTicker{t1, t2}
+}
+
+type compoundTicker struct {
+	t1, t2 Ticker
+}
+
+func (ticker *compoundTicker) Chan() <-chan time.Time {
+	ch := make(chan time.Time)
+	go func() {
+		defer ticker.Stop()
+		for {
+			select {
+			case v, ok := <-ticker.t1.Chan():
+				if !ok {
+					return
+				}
+				ch <- v
+			case v, ok := <-ticker.t2.Chan():
+				if !ok {
+					return
+				}
+				ch <- v
+			}
+		}
+	}()
+	return ch
+}
+
+func (ticker *compoundTicker) Stop() {
+	ticker.t1.Stop()
+	ticker.t2.Stop()
+}
diff --git a/pkg/guestagent/ticker/ebpf_linux.go b/pkg/guestagent/ticker/ebpf_linux.go
new file mode 100644
index 00000000..8bd4ddee
--- /dev/null
+++ b/pkg/guestagent/ticker/ebpf_linux.go
@@ -0,0 +1,135 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package ticker
+
+import (
+	"strings"
+	"time"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/asm"
+	"github.com/cilium/ebpf/link"
+	"github.com/cilium/ebpf/ringbuf"
+	"github.com/sirupsen/logrus"
+)
+
+func NewEbpfTicker(tracepoints []string) (Ticker, error) {
+	var (
+		ticker ebpfTicker
+		err    error
+	)
+	ticker.events, err = ebpf.NewMap(&ebpf.MapSpec{
+		Name:       "lima_ticker_events",
+		Type:       ebpf.RingBuf,
+		MaxEntries: 1 << 20,
+	})
+	if err != nil {
+		ticker.Stop()
+		return nil, err
+	}
+
+	ticker.prog, err = buildEbpfProg(ticker.events)
+	if err != nil {
+		ticker.Stop()
+		return nil, err
+	}
+
+	for _, tp := range tracepoints {
+		tpPair := strings.SplitN(tp, ":", 2)
+		tpLink, err := link.Tracepoint(tpPair[0], tpPair[1], ticker.prog, nil)
+		if err != nil {
+			ticker.Stop()
+			return nil, err
+		}
+		ticker.links = append(ticker.links, tpLink)
+	}
+
+	ticker.reader, err = ringbuf.NewReader(ticker.events)
+	if err != nil {
+		ticker.Stop()
+		return nil, err
+	}
+
+	ticker.ch = make(chan time.Time)
+	go func() {
+		for {
+			_, rdErr := ticker.reader.Read()
+			if rdErr != nil {
+				logrus.WithError(rdErr).Warn("ebpfTicker: failed to read ringbuf")
+				ticker.Stop()
+				return
+			}
+			ticker.ch <- time.Now()
+		}
+	}()
+
+	return &ticker, nil
+}
+
+type ebpfTicker struct {
+	events *ebpf.Map
+	prog   *ebpf.Program
+	links  []link.Link
+	reader *ringbuf.Reader
+	ch     chan time.Time
+}
+
+func (ticker *ebpfTicker) Chan() <-chan time.Time {
+	return ticker.ch
+}
+
+func (ticker *ebpfTicker) Stop() {
+	if ticker.events != nil {
+		_ = ticker.events.Close()
+	}
+	if ticker.prog != nil {
+		_ = ticker.prog.Close()
+	}
+	for _, l := range ticker.links {
+		_ = l.Close()
+	}
+	if ticker.reader != nil {
+		_ = ticker.reader.Close()
+	}
+	if ticker.ch != nil {
+		close(ticker.ch)
+	}
+}
+
+func buildEbpfProg(events *ebpf.Map) (*ebpf.Program, error) {
+	inst := asm.Instructions{
+		// ringbuf = &map
+		asm.LoadMapPtr(asm.R1, events.FD()),
+
+		// data = FP - 8
+		asm.Mov.Reg(asm.R2, asm.R10),
+		asm.Add.Imm(asm.R2, -8),
+
+		// *data = 1
+		asm.StoreImm(asm.R2, 0, 1, asm.Word),
+
+		// size = 1
+		asm.Mov.Imm(asm.R3, 1),
+
+		// flags = 0
+		asm.Mov.Imm(asm.R4, 0),
+
+		// long bpf_ringbuf_output(void *ringbuf, void *data, u64 size, u64 flags)
+		// https://man7.org/linux/man-pages/man7/bpf-helpers.7.html
+		asm.FnRingbufOutput.Call(),
+
+		// return 0
+		asm.Mov.Imm(asm.R0, 0),
+		asm.Return(),
+	}
+
+	spec := &ebpf.ProgramSpec{
+		Name:         "lima_ticker",
+		Type:         ebpf.TracePoint,
+		License:      "Apache-2.0", // No need to be GPL?
+		Instructions: inst,
+	}
+
+	return ebpf.NewProgram(spec)
+}
diff --git a/pkg/guestagent/ticker/simple.go b/pkg/guestagent/ticker/simple.go
new file mode 100644
index 00000000..c9b3446e
--- /dev/null
+++ b/pkg/guestagent/ticker/simple.go
@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package ticker
+
+import (
+	"time"
+)
+
+func NewSimpleTicker(ticker *time.Ticker) Ticker {
+	return &simpleTicker{Ticker: ticker}
+}
+
+type simpleTicker struct {
+	*time.Ticker
+}
+
+func (ticker *simpleTicker) Chan() <-chan time.Time {
+	return ticker.Ticker.C
+}
diff --git a/pkg/guestagent/ticker/ticker.go b/pkg/guestagent/ticker/ticker.go
new file mode 100644
index 00000000..edfa5012
--- /dev/null
+++ b/pkg/guestagent/ticker/ticker.go
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package ticker
+
+import (
+	"time"
+)
+
+type Ticker interface {
+	Chan() <-chan time.Time
+	Stop()
+}
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index 6a3163ba..b53848db 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -163,11 +163,19 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o
 	vSockPort := limaDriver.Info().VsockPort
 	virtioPort := limaDriver.Info().VirtioPort
 	noCloudInit := limaDriver.Info().Features.NoCloudInit
+	rosettaEnabled := limaDriver.Info().Features.RosettaEnabled
+	rosettaBinFmt := limaDriver.Info().Features.RosettaBinFmt
+
+	// Disable Rosetta in Plain mode
+	if *inst.Config.Plain {
+		rosettaEnabled = false
+		rosettaBinFmt = false
+	}
 
 	if err := cidata.GenerateCloudConfig(ctx, inst.Dir, instName, inst.Config); err != nil {
 		return nil, err
 	}
-	if err := cidata.GenerateISO9660(ctx, limaDriver, inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort, noCloudInit); err != nil {
+	if err := cidata.GenerateISO9660(ctx, limaDriver, inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort, noCloudInit, rosettaEnabled, rosettaBinFmt); err != nil {
 		return nil, err
 	}
 
diff --git a/pkg/limactlutil/limactlutil.go b/pkg/limactlutil/limactlutil.go
new file mode 100644
index 00000000..c4d2b766
--- /dev/null
+++ b/pkg/limactlutil/limactlutil.go
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package limactlutil
+
+import (
+	"bytes"
+	"cmp"
+	"context"
+	"encoding/json"
+	"fmt"
+	"os"
+	"os/exec"
+
+	"github.com/lima-vm/lima/v2/pkg/limatype"
+)
+
+// Path returns the path to the `limactl` executable.
+func Path() (string, error) {
+	limactl := cmp.Or(os.Getenv("LIMACTL"), "limactl")
+	return exec.LookPath(limactl)
+}
+
+// Inspect runs `limactl list --json INST` and parses the output.
+func Inspect(ctx context.Context, limactl, instName string) (*limatype.Instance, error) {
+	var stdout, stderr bytes.Buffer
+	cmd := exec.CommandContext(ctx, limactl, "list", "--json", instName)
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	if err := cmd.Run(); err != nil {
+		return nil, fmt.Errorf("failed to run %v: stdout=%q, stderr=%q: %w", cmd.Args, stdout.String(), stderr.String(), err)
+	}
+	var inst limatype.Instance
+	if err := json.Unmarshal(stdout.Bytes(), &inst); err != nil {
+		return nil, err
+	}
+	return &inst, nil
+}
diff --git a/pkg/limainfo/limainfo.go b/pkg/limainfo/limainfo.go
index b1f27e44..93235c84 100644
--- a/pkg/limainfo/limainfo.go
+++ b/pkg/limainfo/limainfo.go
@@ -17,6 +17,7 @@ import (
 	"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
 	"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
 	"github.com/lima-vm/lima/v2/pkg/limayaml"
+	"github.com/lima-vm/lima/v2/pkg/plugin"
 	"github.com/lima-vm/lima/v2/pkg/registry"
 	"github.com/lima-vm/lima/v2/pkg/templatestore"
 	"github.com/lima-vm/lima/v2/pkg/usrlocalsharelima"
@@ -35,6 +36,7 @@ type LimaInfo struct {
 	HostOS          string                       `json:"hostOS"`       // since Lima v2.0.0
 	HostArch        string                       `json:"hostArch"`     // since Lima v2.0.0
 	IdentityFile    string                       `json:"identityFile"` // since Lima v2.0.0
+	Plugins         []plugin.Plugin              `json:"plugins"`      // since Lima v2.0.0
 }
 
 type DriverExt struct {
@@ -108,5 +110,15 @@ func New(ctx context.Context) (*LimaInfo, error) {
 			Location: bin,
 		}
 	}
+
+	plugins, err := plugin.DiscoverPlugins()
+	if err != nil {
+		logrus.WithError(err).Warn("Failed to discover plugins")
+		// Don't fail the entire info command if plugin discovery fails.
+		info.Plugins = []plugin.Plugin{}
+	} else {
+		info.Plugins = plugins
+	}
+
 	return info, nil
 }
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index c0d95eff..9e546d72 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -631,21 +631,39 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin
 	// Combine all mounts; highest priority entry determines writable status.
 	// Only works for exact matches; does not normalize case or resolve symlinks.
 	mounts := make([]limatype.Mount, 0, len(d.Mounts)+len(y.Mounts)+len(o.Mounts))
-	location := make(map[string]int)
+	mountPoint := make(map[string]int)
 	for _, mount := range slices.Concat(d.Mounts, y.Mounts, o.Mounts) {
 		if out, err := executeHostTemplate(mount.Location, instDir, y.Param); err == nil {
 			mount.Location = filepath.Clean(out.String())
 		} else {
 			logrus.WithError(err).Warnf("Couldn't process mount location %q as a template", mount.Location)
 		}
-		if mount.MountPoint != nil {
+		// Expand a path that begins with `~`. Relative paths are not modified, and rejected by Validate() later.
+		if localpathutil.IsTildePath(mount.Location) {
+			if location, err := localpathutil.Expand(mount.Location); err == nil {
+				mount.Location = location
+			} else {
+				logrus.WithError(err).Warnf("Couldn't expand location %q", mount.Location)
+			}
+		}
+		if mount.MountPoint == nil {
+			mountLocation := mount.Location
+			if runtime.GOOS == "windows" {
+				var err error
+				mountLocation, err = ioutilx.WindowsSubsystemPath(ctx, mountLocation)
+				if err != nil {
+					logrus.WithError(err).Warnf("Couldn't convert location %q into mount target", mount.Location)
+				}
+			}
+			mount.MountPoint = ptr.Of(mountLocation)
+		} else {
 			if out, err := executeGuestTemplate(*mount.MountPoint, instDir, y.User, y.Param); err == nil {
 				mount.MountPoint = ptr.Of(out.String())
 			} else {
 				logrus.WithError(err).Warnf("Couldn't process mount point %q as a template", *mount.MountPoint)
 			}
 		}
-		if i, ok := location[mount.Location]; ok {
+		if i, ok := mountPoint[*mount.MountPoint]; ok {
 			if mount.SSHFS.Cache != nil {
 				mounts[i].SSHFS.Cache = mount.SSHFS.Cache
 			}
@@ -677,7 +695,7 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin
 				mounts[i].MountPoint = mount.MountPoint
 			}
 		} else {
-			location[mount.Location] = len(mounts)
+			mountPoint[*mount.MountPoint] = len(mounts)
 			mounts = append(mounts, mount)
 		}
 	}
@@ -713,26 +731,6 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin
 				mounts[i].NineP.Cache = ptr.Of(Default9pCacheForRO)
 			}
 		}
-
-		// Expand a path that begins with `~`. Relative paths are not modified, and rejected by Validate() later.
-		if localpathutil.IsTildePath(mount.Location) {
-			if location, err := localpathutil.Expand(mount.Location); err == nil {
-				mounts[i].Location = location
-			} else {
-				logrus.WithError(err).Warnf("Couldn't expand location %q", mount.Location)
-			}
-		}
-		if mount.MountPoint == nil {
-			mountLocation := mounts[i].Location
-			if runtime.GOOS == "windows" {
-				var err error
-				mountLocation, err = ioutilx.WindowsSubsystemPath(ctx, mountLocation)
-				if err != nil {
-					logrus.WithError(err).Warnf("Couldn't convert location %q into mount target", mounts[i].Location)
-				}
-			}
-			mounts[i].MountPoint = ptr.Of(mountLocation)
-		}
 	}
 
 	// Note: DNS lists are not combined; highest priority setting is picked
@@ -768,22 +766,6 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin
 	y.CACertificates.Files = unique(slices.Concat(d.CACertificates.Files, y.CACertificates.Files, o.CACertificates.Files))
 	y.CACertificates.Certs = unique(slices.Concat(d.CACertificates.Certs, y.CACertificates.Certs, o.CACertificates.Certs))
 
-	if runtime.GOOS == "darwin" && IsNativeArch(limatype.AARCH64) {
-		if y.VMOpts.VZ.Rosetta.Enabled == nil {
-			y.VMOpts.VZ.Rosetta.Enabled = d.VMOpts.VZ.Rosetta.Enabled
-		}
-		if o.VMOpts.VZ.Rosetta.Enabled != nil {
-			y.VMOpts.VZ.Rosetta.Enabled = o.VMOpts.VZ.Rosetta.Enabled
-		}
-	}
-
-	if y.VMOpts.VZ.Rosetta.BinFmt == nil {
-		y.VMOpts.VZ.Rosetta.BinFmt = d.VMOpts.VZ.Rosetta.BinFmt
-	}
-	if o.VMOpts.VZ.Rosetta.BinFmt != nil {
-		y.VMOpts.VZ.Rosetta.BinFmt = o.VMOpts.VZ.Rosetta.BinFmt
-	}
-
 	if y.NestedVirtualization == nil {
 		y.NestedVirtualization = d.NestedVirtualization
 	}
@@ -831,8 +813,6 @@ func fixUpForPlainMode(y *limatype.LimaYAML) {
 	y.Mounts = nil
 	y.Containerd.System = ptr.Of(false)
 	y.Containerd.User = ptr.Of(false)
-	y.VMOpts.VZ.Rosetta.BinFmt = ptr.Of(false)
-	y.VMOpts.VZ.Rosetta.Enabled = ptr.Of(false)
 	y.TimeZone = ptr.Of("")
 }
 
@@ -922,14 +902,6 @@ func FillPortForwardDefaults(rule *limatype.PortForward, instDir string, user li
 			rule.GuestPortRange[1] = rule.GuestPort
 		}
 	}
-	if rule.HostPortRange[0] == 0 && rule.HostPortRange[1] == 0 {
-		if rule.HostPort == 0 {
-			rule.HostPortRange = rule.GuestPortRange
-		} else {
-			rule.HostPortRange[0] = rule.HostPort
-			rule.HostPortRange[1] = rule.HostPort
-		}
-	}
 	if rule.GuestSocket != "" {
 		if out, err := executeGuestTemplate(rule.GuestSocket, instDir, user, param); err == nil {
 			rule.GuestSocket = out.String()
@@ -946,6 +918,13 @@ func FillPortForwardDefaults(rule *limatype.PortForward, instDir string, user li
 		if !filepath.IsAbs(rule.HostSocket) {
 			rule.HostSocket = filepath.Join(instDir, filenames.SocketDir, rule.HostSocket)
 		}
+	} else if rule.HostPortRange[0] == 0 && rule.HostPortRange[1] == 0 {
+		if rule.HostPort == 0 {
+			rule.HostPortRange = rule.GuestPortRange
+		} else {
+			rule.HostPortRange[0] = rule.HostPort
+			rule.HostPortRange[1] = rule.HostPort
+		}
 	}
 }
 
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index d38d55e1..87ffb7e6 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -314,8 +314,10 @@ func Validate(y *limatype.LimaYAML, warn bool) error {
 			if err := validatePort(fmt.Sprintf("%s.guestPortRange[%d]", field, j), rule.GuestPortRange[j]); err != nil {
 				errs = errors.Join(errs, err)
 			}
-			if err := validatePort(fmt.Sprintf("%s.hostPortRange[%d]", field, j), rule.HostPortRange[j]); err != nil {
-				errs = errors.Join(errs, err)
+			if rule.HostSocket == "" {
+				if err := validatePort(fmt.Sprintf("%s.hostPortRange[%d]", field, j), rule.HostPortRange[j]); err != nil {
+					errs = errors.Join(errs, err)
+				}
 			}
 		}
 		if rule.GuestPortRange[0] > rule.GuestPortRange[1] {
@@ -324,9 +326,6 @@ func Validate(y *limatype.LimaYAML, warn bool) error {
 		if rule.HostPortRange[0] > rule.HostPortRange[1] {
 			errs = errors.Join(errs, fmt.Errorf("field `%s.hostPortRange[1]` must be greater than or equal to field `%s.hostPortRange[0]`", field, field))
 		}
-		if rule.GuestPortRange[1]-rule.GuestPortRange[0] != rule.HostPortRange[1]-rule.HostPortRange[0] {
-			errs = errors.Join(errs, fmt.Errorf("field `%s.hostPortRange` must specify the same number of ports as field `%s.guestPortRange`", field, field))
-		}
 		if rule.GuestSocket != "" {
 			if !path.IsAbs(rule.GuestSocket) {
 				errs = errors.Join(errs, fmt.Errorf("field `%s.guestSocket` must be an absolute path, but is %q", field, rule.GuestSocket))
@@ -343,7 +342,10 @@ func Validate(y *limatype.LimaYAML, warn bool) error {
 			if rule.GuestSocket == "" && rule.GuestPortRange[1]-rule.GuestPortRange[0] > 0 {
 				errs = errors.Join(errs, fmt.Errorf("field `%s.hostSocket` can only be mapped from a single port or socket. not a range", field))
 			}
+		} else if rule.GuestPortRange[1]-rule.GuestPortRange[0] != rule.HostPortRange[1]-rule.HostPortRange[0] {
+			errs = errors.Join(errs, fmt.Errorf("field `%s.hostPortRange` must specify the same number of ports as field `%s.guestPortRange`", field, field))
 		}
+
 		if len(rule.HostSocket) >= osutil.UnixPathMax {
 			errs = errors.Join(errs, fmt.Errorf("field `%s.hostSocket` must be less than UNIX_PATH_MAX=%d characters, but is %d",
 				field, osutil.UnixPathMax, len(rule.HostSocket)))
diff --git a/pkg/mcp/msi/filesystem.go b/pkg/mcp/msi/filesystem.go
new file mode 100644
index 00000000..f15321cc
--- /dev/null
+++ b/pkg/mcp/msi/filesystem.go
@@ -0,0 +1,83 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+// Portion of AI prompt texts from:
+// - https://github.com/google-gemini/gemini-cli/blob/v0.1.12/docs/tools/file-system.md
+//
+// SPDX-FileCopyrightText: Copyright 2025 Google LLC
+
+package msi
+
+import (
+	"io/fs"
+	"time"
+
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+var ListDirectory = &mcp.Tool{
+	Name:        "list_directory",
+	Description: `Lists the names of files and subdirectories directly within a specified directory path.`,
+}
+
+type ListDirectoryParams struct {
+	Path string `json:"path" jsonschema:"The absolute path to the directory to list."`
+}
+
+// ListDirectoryResultEntry is similar to [io/fs.FileInfo].
+type ListDirectoryResultEntry struct {
+	Name    string       `json:"name" jsonschema:"base name of the file"`
+	Size    *int64       `json:"size,omitempty" jsonschema:"length in bytes for regular files; system-dependent for others"`
+	Mode    *fs.FileMode `json:"mode,omitempty" jsonschema:"file mode bits"`
+	ModTime *time.Time   `json:"time,omitempty" jsonschema:"modification time"`
+	IsDir   *bool        `json:"is_dir,omitempty" jsonschema:"true for a directory"`
+}
+
+type ListDirectoryResult struct {
+	Entries []ListDirectoryResultEntry `json:"entries" jsonschema:"The directory content entries."`
+}
+
+var ReadFile = &mcp.Tool{
+	Name:        "read_file",
+	Description: `Reads and returns the content of a specified file.`,
+}
+
+type ReadFileParams struct {
+	Path string `json:"path" jsonschema:"The absolute path to the file to read."`
+	// TODO: Offset *int   `json:"offset,omitempty" jsonschema:"For text files, the 0-based line number to start reading from. Requires limit to be set."`
+	// TODO: Limit  *int   `json:"limit,omitempty" jsonschema:"For text files, the maximum number of lines to read. If omitted, reads a default maximum (e.g., 2000 lines) or the entire file if feasible."`
+}
+
+var WriteFile = &mcp.Tool{
+	Name:        "write_file",
+	Description: `Writes content to a specified file. If the file exists, it will be overwritten. If the file doesn't exist, it (and any necessary parent directories) will be created.`,
+}
+
+type WriteFileParams struct {
+	Path    string `json:"path" jsonschema:"The absolute path to the file to write to."`
+	Content string `json:"content" jsonschema:"The content to write into the file."`
+}
+
+var Glob = &mcp.Tool{
+	Name:        "glob",
+	Description: `Finds files matching specific glob patterns (e.g., src/**/*.ts, *.md)`, // Not sorted by mod time, unlike Gemini
+}
+
+type GlobParams struct {
+	Pattern string  `json:"pattern" jsonschema:"The glob pattern to match against (e.g., '*.py', 'src/**/*.js')."`
+	Path    *string `json:"path,omitempty" jsonschema:"The absolute path to the directory to search within. If omitted, searches the tool's root directory."`
+	// TODO: CaseSensitive bool    `json:"case_sensitive,omitempty" jsonschema:": Whether the search should be case-sensitive. Defaults to false."`
+}
+
+var SearchFileContent = &mcp.Tool{
+	Name:        "search_file_content",
+	Description: `Searches for a regular expression pattern within the content of files in a specified directory. Internally calls 'git grep -n --no-index'.`,
+}
+
+type SearchFileContentParams struct {
+	Pattern string  `json:"pattern" jsonschema:"The regular expression (regex) to search for (e.g., 'function\\s+myFunction')."`
+	Path    *string `json:"path,omitempty" jsonschema:"The absolute path to the directory to search within. Defaults to the current working directory."`
+	Include *string `json:"include,omitempty" jsonschema:"A glob pattern to filter which files are searched (e.g., '*.js', 'src/**/*.{ts,tsx}'). If omitted, searches most files (respecting common ignores)."`
+}
+
+// TODO: implement Replace
diff --git a/pkg/mcp/msi/msi.go b/pkg/mcp/msi/msi.go
new file mode 100644
index 00000000..fb59ac42
--- /dev/null
+++ b/pkg/mcp/msi/msi.go
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+// Package msi provides the "MCP Sandbox Interface" (tentative)
+// that should be reusable for other projects too.
+//
+// MCP Sandbox Interface defines MCP (Model Context Protocol) tools
+// that can be used for reading, writing, and executing local files
+// with an appropriate sandboxing technology. The sandboxing technology
+// can be more secure and/or efficient than the default tools provided
+// by an AI agent.
+//
+// MCP Sandbox Interface was inspired by Gemini CLI's built-in tools.
+// https://github.com/google-gemini/gemini-cli/tree/v0.1.12/docs/tools
+//
+// Notable differences from Gemini CLI's built-in tools:
+//   - the output format is JSON, not a plain text
+//   - the output of [SearchFileContent] always corresponds to `git grep -n --no-index`
+//   - [RunShellCommandParams].Command is a string slice, not a string
+//   - [RunShellCommandParams].Directory is an absolute path, not a relative path
+//   - [RunShellCommandParams].Directory must not be empty
+//
+// Eventually, this package may be split to a separate repository.
+package msi
diff --git a/pkg/mcp/msi/shell.go b/pkg/mcp/msi/shell.go
new file mode 100644
index 00000000..e53ed1c8
--- /dev/null
+++ b/pkg/mcp/msi/shell.go
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+// Portion of AI prompt texts from:
+// - https://github.com/google-gemini/gemini-cli/blob/v0.1.12/docs/tools/shell.md
+//
+// SPDX-FileCopyrightText: Copyright 2025 Google LLC
+
+package msi
+
+import "github.com/modelcontextprotocol/go-sdk/mcp"
+
+var RunShellCommand = &mcp.Tool{
+	Name:        "run_shell_command",
+	Description: `Executes a given shell command.`,
+}
+
+type RunShellCommandParams struct {
+	Command     []string `json:"command" jsonschema:"The exact shell command to execute. Defined as a string slice, unlike Gemini's run_shell_command that defines it as a single string."`
+	Description string   `json:"description,omitempty" jsonschema:"A brief description of the command's purpose, which will be potentially shown to the user."`
+	Directory   string   `json:"directory" jsonschema:"The absolute directory in which to execute the command. Unlike Gemini's run_shell_command, this must not be a relative path, and must not be empty."`
+}
+
+type RunShellCommandResult struct {
+	Stdout   string `json:"stdout" jsonschema:"Output from the standard output stream."`
+	Stderr   string `json:"stderr" jsonschema:"Output from the standard error stream."`
+	Error    string `json:"error,omitempty" jsonschema:"Any error message reported by the subprocess."`
+	ExitCode *int   `json:"exit_code,omitempty" jsonschema:"Exit code of the command."`
+}
diff --git a/pkg/mcp/toolset/filesystem.go b/pkg/mcp/toolset/filesystem.go
new file mode 100644
index 00000000..0b2bc949
--- /dev/null
+++ b/pkg/mcp/toolset/filesystem.go
@@ -0,0 +1,165 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package toolset
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"io"
+	"os"
+	"path"
+
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+
+	"github.com/lima-vm/lima/v2/pkg/mcp/msi"
+	"github.com/lima-vm/lima/v2/pkg/ptr"
+)
+
+func (ts *ToolSet) ListDirectory(ctx context.Context,
+	_ *mcp.CallToolRequest, args msi.ListDirectoryParams,
+) (*mcp.CallToolResult, any, error) {
+	if ts.inst == nil {
+		return nil, nil, errors.New("instance not registered")
+	}
+	guestPath, err := ts.TranslateHostPath(args.Path)
+	if err != nil {
+		return nil, nil, err
+	}
+	guestEnts, err := ts.sftp.ReadDirContext(ctx, guestPath)
+	if err != nil {
+		return nil, nil, err
+	}
+	res := msi.ListDirectoryResult{
+		Entries: make([]msi.ListDirectoryResultEntry, len(guestEnts)),
+	}
+	for i, f := range guestEnts {
+		res.Entries[i].Name = f.Name()
+		res.Entries[i].Size = ptr.Of(f.Size())
+		res.Entries[i].Mode = ptr.Of(f.Mode())
+		res.Entries[i].ModTime = ptr.Of(f.ModTime())
+		res.Entries[i].IsDir = ptr.Of(f.IsDir())
+	}
+	resJ, err := json.Marshal(res)
+	if err != nil {
+		return nil, nil, err
+	}
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{Text: string(resJ)}},
+	}, nil, nil
+}
+
+func (ts *ToolSet) ReadFile(_ context.Context,
+	_ *mcp.CallToolRequest, args msi.ReadFileParams,
+) (*mcp.CallToolResult, any, error) {
+	if ts.inst == nil {
+		return nil, nil, errors.New("instance not registered")
+	}
+	guestPath, err := ts.TranslateHostPath(args.Path)
+	if err != nil {
+		return nil, nil, err
+	}
+	f, err := ts.sftp.Open(guestPath)
+	if err != nil {
+		return nil, nil, err
+	}
+	defer f.Close()
+	const limitBytes = 32 * 1024 * 1024
+	lr := io.LimitReader(f, limitBytes)
+	b, err := io.ReadAll(lr)
+	if err != nil {
+		return nil, nil, err
+	}
+	return &mcp.CallToolResult{
+		// Gemini:
+		// For text files: The file content, potentially prefixed with a truncation message
+		// (e.g., [File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...).
+		Content: []mcp.Content{&mcp.TextContent{Text: string(b)}},
+	}, nil, nil
+}
+
+func (ts *ToolSet) WriteFile(_ context.Context,
+	_ *mcp.CallToolRequest, args msi.WriteFileParams,
+) (*mcp.CallToolResult, any, error) {
+	if ts.inst == nil {
+		return nil, nil, errors.New("instance not registered")
+	}
+	guestPath, err := ts.TranslateHostPath(args.Path)
+	if err != nil {
+		return nil, nil, err
+	}
+	f, err := ts.sftp.Create(guestPath)
+	if err != nil {
+		return nil, nil, err
+	}
+	defer f.Close()
+	_, err = f.Write([]byte(args.Content))
+	if err != nil {
+		return nil, nil, err
+	}
+	return &mcp.CallToolResult{
+		// Gemini:
+		// A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt`
+		// or `Successfully created and wrote to new file: /path/to/new/file.txt.`
+		Content: []mcp.Content{&mcp.TextContent{Text: "OK"}},
+	}, nil, nil
+}
+
+func (ts *ToolSet) Glob(_ context.Context,
+	_ *mcp.CallToolRequest, args msi.GlobParams,
+) (*mcp.CallToolResult, any, error) {
+	if ts.inst == nil {
+		return nil, nil, errors.New("instance not registered")
+	}
+	pathStr, err := os.Getwd()
+	if err != nil {
+		return nil, nil, err
+	}
+	if args.Path != nil && *args.Path != "" {
+		pathStr = *args.Path
+	}
+	guestPath, err := ts.TranslateHostPath(pathStr)
+	if err != nil {
+		return nil, nil, err
+	}
+	pattern := path.Join(guestPath, args.Pattern)
+	matches, err := ts.sftp.Glob(pattern)
+	if err != nil {
+		return nil, nil, err
+	}
+	resJ, err := json.Marshal(matches)
+	if err != nil {
+		return nil, nil, err
+	}
+	return &mcp.CallToolResult{
+		// Gemini:
+		// A message like: Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts...
+		Content: []mcp.Content{&mcp.TextContent{Text: string(resJ)}},
+	}, nil, nil
+}
+
+func (ts *ToolSet) SearchFileContent(ctx context.Context,
+	req *mcp.CallToolRequest, args msi.SearchFileContentParams,
+) (*mcp.CallToolResult, any, error) {
+	if ts.inst == nil {
+		return nil, nil, errors.New("instance not registered")
+	}
+	pathStr, err := os.Getwd()
+	if err != nil {
+		return nil, nil, err
+	}
+	if args.Path != nil && *args.Path != "" {
+		pathStr = *args.Path
+	}
+	guestPath, err := ts.TranslateHostPath(pathStr)
+	if err != nil {
+		return nil, nil, err
+	}
+	if args.Include != nil && *args.Include != "" {
+		guestPath = path.Join(guestPath, *args.Include)
+	}
+	return ts.RunShellCommand(ctx, req, msi.RunShellCommandParams{
+		Command: []string{"git", "grep", "-n", "--no-index", args.Pattern, guestPath},
+	})
+}
diff --git a/pkg/mcp/toolset/shell.go b/pkg/mcp/toolset/shell.go
new file mode 100644
index 00000000..eb00cbb5
--- /dev/null
+++ b/pkg/mcp/toolset/shell.go
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package toolset
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"os/exec"
+
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+
+	"github.com/lima-vm/lima/v2/pkg/mcp/msi"
+	"github.com/lima-vm/lima/v2/pkg/ptr"
+)
+
+func (ts *ToolSet) RunShellCommand(ctx context.Context,
+	_ *mcp.CallToolRequest, args msi.RunShellCommandParams,
+) (*mcp.CallToolResult, any, error) {
+	if ts.inst == nil {
+		return nil, nil, errors.New("instance not registered")
+	}
+	guestPath, err := ts.TranslateHostPath(args.Directory)
+	if err != nil {
+		return nil, nil, err
+	}
+	cmd := exec.CommandContext(ctx, ts.limactl,
+		append([]string{"shell", "--workdir=" + guestPath, ts.inst.Name},
+			args.Command...)...)
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	cmdErr := cmd.Run()
+	res := msi.RunShellCommandResult{
+		Stdout: stdout.String(),
+		Stderr: stderr.String(),
+	}
+	if cmdErr == nil {
+		res.ExitCode = ptr.Of(0)
+	} else {
+		res.Error = cmdErr.Error()
+		if st := cmd.ProcessState; st != nil {
+			res.ExitCode = ptr.Of(st.ExitCode())
+		}
+	}
+	resJ, err := json.Marshal(res)
+	if err != nil {
+		return nil, nil, err
+	}
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{Text: string(resJ)}},
+	}, nil, nil
+}
diff --git a/pkg/mcp/toolset/toolset.go b/pkg/mcp/toolset/toolset.go
new file mode 100644
index 00000000..aa384da6
--- /dev/null
+++ b/pkg/mcp/toolset/toolset.go
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package toolset
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"slices"
+
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+	"github.com/pkg/sftp"
+	"github.com/sirupsen/logrus"
+
+	"github.com/lima-vm/lima/v2/pkg/instance/hostname"
+	"github.com/lima-vm/lima/v2/pkg/limatype"
+	"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
+	"github.com/lima-vm/lima/v2/pkg/mcp/msi"
+	"github.com/lima-vm/lima/v2/pkg/sshutil"
+)
+
+func New(limactl string) (*ToolSet, error) {
+	ts := &ToolSet{
+		limactl: limactl,
+	}
+	return ts, nil
+}
+
+type ToolSet struct {
+	limactl string // needed for `limactl shell --workdir`
+
+	// Set on RegisterInstance()
+	inst    *limatype.Instance
+	sftp    *sftp.Client
+	sftpCmd *exec.Cmd
+}
+
+func newSFTPClient(ctx context.Context, inst *limatype.Instance) (*sftp.Client, *exec.Cmd, error) {
+	sshExe, err := sshutil.NewSSHExe()
+	if err != nil {
+		return nil, nil, err
+	}
+	args := slices.Concat(sshExe.Args, []string{"-F", filepath.Join(inst.Dir, filenames.SSHConfig), hostname.FromInstName(inst.Name), "-s", "sftp"})
+	cmd := exec.CommandContext(ctx, sshExe.Exe, args...)
+	cmd.Stderr = os.Stderr
+	w, err := cmd.StdinPipe()
+	if err != nil {
+		return nil, nil, err
+	}
+	r, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, nil, err
+	}
+	if err = cmd.Start(); err != nil {
+		return nil, nil, err
+	}
+	client, err := sftp.NewClientPipe(r, w)
+	if err != nil {
+		if cmd != nil && cmd.Process != nil {
+			_ = cmd.Process.Kill()
+		}
+	}
+	return client, cmd, err
+}
+
+func (ts *ToolSet) RegisterInstance(ctx context.Context, inst *limatype.Instance) error {
+	if inst.Status != limatype.StatusRunning {
+		return fmt.Errorf("expected status of instance %q to be %q, got %q",
+			inst.Name, limatype.StatusRunning, inst.Status)
+	}
+	if len(inst.Config.Mounts) == 0 {
+		logrus.Warnf("instance %q has no mount", inst.Name)
+	}
+	ts.inst = inst
+	var err error
+	ts.sftp, ts.sftpCmd, err = newSFTPClient(ctx, inst)
+	return err
+}
+
+func (ts *ToolSet) RegisterServer(server *mcp.Server) error {
+	mcp.AddTool(server, msi.ListDirectory, ts.ListDirectory)
+	mcp.AddTool(server, msi.ReadFile, ts.ReadFile)
+	mcp.AddTool(server, msi.WriteFile, ts.WriteFile)
+	mcp.AddTool(server, msi.Glob, ts.Glob)
+	mcp.AddTool(server, msi.SearchFileContent, ts.SearchFileContent)
+	mcp.AddTool(server, msi.RunShellCommand, ts.RunShellCommand)
+	return nil
+}
+
+func (ts *ToolSet) Close() error {
+	var err error
+	if ts.sftp != nil {
+		err = errors.Join(err, ts.sftp.Close())
+	}
+	if ts.sftpCmd != nil && ts.sftpCmd.Process != nil {
+		err = errors.Join(err, ts.sftpCmd.Process.Kill())
+	}
+	return err
+}
+
+func (ts *ToolSet) TranslateHostPath(hostPath string) (string, error) {
+	if hostPath == "" {
+		return "", errors.New("path is empty")
+	}
+	if !filepath.IsAbs(hostPath) {
+		return "", fmt.Errorf("expected an absolute path, got a relative path: %q", hostPath)
+	}
+	// TODO: make sure that hostPath is mounted
+	return hostPath, nil
+}
diff --git a/pkg/networks/usernet/client.go b/pkg/networks/usernet/client.go
index f556c0a0..6a2437c3 100644
--- a/pkg/networks/usernet/client.go
+++ b/pkg/networks/usernet/client.go
@@ -38,9 +38,11 @@ func (c *Client) ConfigureDriver(ctx context.Context, inst *limatype.Instance, s
 	if err != nil {
 		return err
 	}
-	err = c.ResolveAndForwardSSH(ipAddress, sshLocalPort)
-	if err != nil {
-		return err
+	if sshLocalPort != 0 {
+		err = c.ResolveAndForwardSSH(ipAddress, sshLocalPort)
+		if err != nil {
+			return err
+		}
 	}
 	hosts := inst.Config.HostResolver.Hosts
 	if hosts == nil {
@@ -127,6 +129,30 @@ func (c *Client) Leases(ctx context.Context) (map[string]string, error) {
 	return leases, nil
 }
 
+// WaitOpeningSSHPort Wait until the guest ssh server is available.
+func (c *Client) WaitOpeningSSHPort(ctx context.Context, inst *limatype.Instance) error {
+	// This timeout is based on the maximum wait time for the first essential requirement.
+	timeoutSeconds := 600
+	ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
+	defer cancel()
+	macAddress := limayaml.MACAddress(inst.Dir)
+	ipAddr, err := c.ResolveIPAddress(ctx, macAddress)
+	if err != nil {
+		return err
+	}
+	// -1 avoids both sides timing out simultaneously.
+	u := fmt.Sprintf("%s/extension/wait_port?ip=%s&port=22&timeout=%d", c.base, ipAddr, timeoutSeconds-1)
+	res, err := httpclientutil.Get(ctx, c.client, u)
+	if err != nil {
+		return err
+	}
+	defer res.Body.Close()
+	if res.StatusCode != http.StatusOK {
+		return errors.New("failed to wait for SSH port")
+	}
+	return nil
+}
+
 func NewClientByName(nwName string) *Client {
 	endpointSock, err := Sock(nwName, EndpointSock)
 	if err != nil {
diff --git a/pkg/networks/usernet/gvproxy.go b/pkg/networks/usernet/gvproxy.go
index 363ac996..89b05286 100644
--- a/pkg/networks/usernet/gvproxy.go
+++ b/pkg/networks/usernet/gvproxy.go
@@ -12,6 +12,7 @@ import (
 	"net/http"
 	"os"
 	"runtime"
+	"strconv"
 	"strings"
 	"time"
 
@@ -103,7 +104,8 @@ func run(ctx context.Context, g *errgroup.Group, configuration *types.Configurat
 	if err != nil {
 		return err
 	}
-	httpServe(ctx, g, ln, vn.Mux())
+
+	httpServe(ctx, g, ln, muxWithExtension(vn))
 
 	if opts.QemuSocket != "" {
 		err = listenQEMU(ctx, vn)
@@ -239,6 +241,58 @@ func httpServe(ctx context.Context, g *errgroup.Group, ln net.Listener, mux http
 	})
 }
 
+func muxWithExtension(n *virtualnetwork.VirtualNetwork) *http.ServeMux {
+	m := n.Mux()
+	m.HandleFunc("/extension/wait_port", func(w http.ResponseWriter, r *http.Request) {
+		ip := r.URL.Query().Get("ip")
+		if net.ParseIP(ip) == nil {
+			msg := fmt.Sprintf("invalid ip address: %s", ip)
+			http.Error(w, msg, http.StatusBadRequest)
+			return
+		}
+		port16, err := strconv.ParseUint(r.URL.Query().Get("port"), 10, 16)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+		port := uint16(port16)
+		addr := fmt.Sprintf("%s:%d", ip, port)
+
+		timeoutSeconds := 10
+		if timeoutString := r.URL.Query().Get("timeout"); timeoutString != "" {
+			timeout16, err := strconv.ParseUint(timeoutString, 10, 16)
+			if err != nil {
+				http.Error(w, err.Error(), http.StatusBadRequest)
+				return
+			}
+			timeoutSeconds = int(timeout16)
+		}
+		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second)
+		defer cancel()
+		// Wait until the port is available.
+		for {
+			conn, err := n.DialContextTCP(ctx, addr)
+			if err == nil {
+				conn.Close()
+				logrus.Debugf("Port is available on %s", addr)
+				w.WriteHeader(http.StatusOK)
+				break
+			}
+			select {
+			case <-ctx.Done():
+				msg := fmt.Sprintf("timed out waiting for port to become available on %s", addr)
+				logrus.Warn(msg)
+				http.Error(w, msg, http.StatusRequestTimeout)
+				return
+			default:
+			}
+			logrus.Debugf("Waiting for port to become available on %s", addr)
+			time.Sleep(1 * time.Second)
+		}
+	})
+	return m
+}
+
 func searchDomains() []string {
 	if runtime.GOOS != "windows" {
 		return resolveSearchDomain("/etc/resolv.conf")
diff --git a/pkg/osutil/exit.go b/pkg/osutil/exit.go
new file mode 100644
index 00000000..ee14ecc2
--- /dev/null
+++ b/pkg/osutil/exit.go
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package osutil
+
+import (
+	"errors"
+	"os"
+	"os/exec"
+)
+
+func HandleExitError(err error) {
+	if err == nil {
+		return
+	}
+
+	var exitErr *exec.ExitError
+	if errors.As(err, &exitErr) {
+		os.Exit(exitErr.ExitCode()) //nolint:revive // it's intentional to call os.Exit in this function
+		return
+	}
+}
diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go
new file mode 100644
index 00000000..81849db1
--- /dev/null
+++ b/pkg/plugin/plugin.go
@@ -0,0 +1,242 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package plugin
+
+import (
+	"cmp"
+	"context"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"slices"
+	"strings"
+
+	"github.com/sirupsen/logrus"
+
+	"github.com/lima-vm/lima/v2/pkg/osutil"
+	"github.com/lima-vm/lima/v2/pkg/usrlocalsharelima"
+)
+
+const defaultPathExt = ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL"
+
+type Plugin struct {
+	Name        string `json:"name"`
+	Path        string `json:"path"`
+	Description string `json:"description,omitempty"`
+}
+
+func DiscoverPlugins() ([]Plugin, error) {
+	var plugins []Plugin
+	seen := make(map[string]bool)
+
+	for _, dir := range getPluginDirectories() {
+		for _, plugin := range scanDirectory(dir) {
+			if !seen[plugin.Name] {
+				plugins = append(plugins, plugin)
+				seen[plugin.Name] = true
+			}
+		}
+	}
+
+	slices.SortFunc(plugins,
+		func(i, j Plugin) int {
+			return cmp.Compare(i.Name, j.Name)
+		})
+
+	return plugins, nil
+}
+
+func getPluginDirectories() []string {
+	dirs := usrlocalsharelima.SelfDirs()
+
+	pathEnv := os.Getenv("PATH")
+	if pathEnv != "" {
+		pathDirs := filepath.SplitList(pathEnv)
+		dirs = append(dirs, pathDirs...)
+	}
+
+	if libexecDir, err := usrlocalsharelima.LibexecLima(); err == nil {
+		if _, err := os.Stat(libexecDir); err == nil {
+			dirs = append(dirs, libexecDir)
+		}
+	}
+
+	return dirs
+}
+
+// isWindowsExecutableExt checks if the given extension is a valid Windows executable extension
+// according to PATHEXT environment variable.
+func isWindowsExecutableExt(ext string) bool {
+	if runtime.GOOS != "windows" {
+		return false
+	}
+
+	pathExt := os.Getenv("PATHEXT")
+	if pathExt == "" {
+		pathExt = defaultPathExt
+	}
+
+	extensions := strings.Split(strings.ToUpper(pathExt), ";")
+	extUpper := strings.ToUpper(ext)
+
+	for _, validExt := range extensions {
+		if validExt == extUpper {
+			return true
+		}
+	}
+	return false
+}
+
+func isExecutable(path string) bool {
+	info, err := os.Stat(path)
+	if err != nil {
+		return false
+	}
+
+	if !info.Mode().IsRegular() {
+		return false
+	}
+
+	if runtime.GOOS != "windows" {
+		return info.Mode()&0o111 != 0
+	}
+
+	ext := strings.ToLower(filepath.Ext(path))
+	pathExt := os.Getenv("PATHEXT")
+	if pathExt == "" {
+		pathExt = defaultPathExt
+	}
+
+	for _, e := range strings.Split(strings.ToLower(pathExt), ";") {
+		if e == ext {
+			return true
+		}
+	}
+	return false
+}
+
+func scanDirectory(dir string) []Plugin {
+	var plugins []Plugin
+
+	entries, err := os.ReadDir(dir)
+	if err != nil {
+		logrus.Debugf("Plugin discovery: failed to scan directory %s: %v", dir, err)
+
+		return plugins
+	}
+
+	for _, entry := range entries {
+		if entry.IsDir() {
+			continue
+		}
+
+		name := entry.Name()
+		if !strings.HasPrefix(name, "limactl-") {
+			continue
+		}
+
+		pluginName := strings.TrimPrefix(name, "limactl-")
+
+		if runtime.GOOS == "windows" {
+			ext := filepath.Ext(pluginName)
+			if isWindowsExecutableExt(ext) {
+				pluginName = strings.TrimSuffix(pluginName, ext)
+			}
+		}
+
+		fullPath := filepath.Join(dir, name)
+
+		if !isExecutable(fullPath) {
+			continue
+		}
+
+		plugin := Plugin{
+			Name: pluginName,
+			Path: fullPath,
+		}
+
+		if desc := extractDescFromScript(fullPath); desc != "" {
+			plugin.Description = desc
+		}
+
+		plugins = append(plugins, plugin)
+	}
+
+	return plugins
+}
+
+func RunExternalPlugin(ctx context.Context, name string, args []string) {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+
+	if err := UpdatePathForPlugins(); err != nil {
+		logrus.Warnf("failed to update PATH environment: %v", err)
+		// PATH update failure shouldn't prevent plugin execution
+	}
+
+	plugins, err := DiscoverPlugins()
+	if err != nil {
+		logrus.Warnf("failed to discover plugins: %v", err)
+		return
+	}
+
+	var execPath string
+	for _, plugin := range plugins {
+		if plugin.Name == name {
+			execPath = plugin.Path
+			break
+		}
+	}
+
+	if execPath == "" {
+		return
+	}
+
+	cmd := exec.CommandContext(ctx, execPath, args...)
+	cmd.Stdin = os.Stdin
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Env = os.Environ()
+
+	err = cmd.Run()
+	osutil.HandleExitError(err)
+	if err == nil {
+		os.Exit(0) //nolint:revive // it's intentional to call os.Exit in this function
+	}
+	logrus.Fatalf("external command %q failed: %v", execPath, err)
+}
+
+var descRegex = regexp.MustCompile(`<limactl-desc>(.*?)</limactl-desc>`)
+
+func extractDescFromScript(path string) string {
+	content, err := os.ReadFile(path)
+	if err != nil {
+		logrus.Debugf("Failed to read plugin script %s: %v", path, err)
+		return ""
+	}
+
+	if !strings.HasPrefix(string(content), "#!") {
+		logrus.Debugf("Plugin %s: not a script file, skipping description extraction", path)
+		return ""
+	}
+
+	matches := descRegex.FindStringSubmatch(string(content))
+	if len(matches) < 2 {
+		logrus.Debugf("Plugin %s: no <limactl-desc> found in script", filepath.Base(path))
+		return ""
+	}
+
+	desc := strings.Trim(matches[1], " ")
+	logrus.Debugf("Plugin %s: extracted description: %q", filepath.Base(path), desc)
+	return desc
+}
+
+func UpdatePathForPlugins() error {
+	pluginDirs := getPluginDirectories()
+	newPath := strings.Join(pluginDirs, string(filepath.ListSeparator))
+	return os.Setenv("PATH", newPath)
+}
diff --git a/pkg/portfwd/listener.go b/pkg/portfwd/listener.go
index f34c1824..f1949ce8 100644
--- a/pkg/portfwd/listener.go
+++ b/pkg/portfwd/listener.go
@@ -8,6 +8,8 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"os"
+	"path/filepath"
 	"strings"
 	"sync"
 
@@ -146,3 +148,13 @@ func (p *ClosableListeners) forwardUDP(ctx context.Context, client *guestagentcl
 func key(protocol, hostAddress, guestAddress string) string {
 	return fmt.Sprintf("%s-%s-%s", protocol, hostAddress, guestAddress)
 }
+
+func prepareUnixSocket(hostSocket string) error {
+	if err := os.RemoveAll(hostSocket); err != nil {
+		return fmt.Errorf("can't clean up %q: %w", hostSocket, err)
+	}
+	if err := os.MkdirAll(filepath.Dir(hostSocket), 0o755); err != nil {
+		return fmt.Errorf("can't create directory for local socket %q: %w", hostSocket, err)
+	}
+	return nil
+}
diff --git a/pkg/portfwd/listener_darwin.go b/pkg/portfwd/listener_darwin.go
index f884550f..ea826f5a 100644
--- a/pkg/portfwd/listener_darwin.go
+++ b/pkg/portfwd/listener_darwin.go
@@ -7,12 +7,26 @@ import (
 	"context"
 	"fmt"
 	"net"
+	"path/filepath"
 	"strconv"
 
 	"github.com/sirupsen/logrus"
 )
 
 func Listen(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.Listener, error) {
+	if filepath.IsAbs(hostAddress) {
+		// Handle Unix domain sockets
+		if err := prepareUnixSocket(hostAddress); err != nil {
+			return nil, err
+		}
+		var lc net.ListenConfig
+		unixLis, err := lc.Listen(ctx, "unix", hostAddress)
+		if err != nil {
+			logrus.WithError(err).Errorf("failed to listen unix: %v", hostAddress)
+			return nil, err
+		}
+		return unixLis, nil
+	}
 	localIPStr, localPortStr, _ := net.SplitHostPort(hostAddress)
 	localIP := net.ParseIP(localIPStr)
 	localPort, _ := strconv.Atoi(localPortStr)
diff --git a/pkg/portfwd/listener_others.go b/pkg/portfwd/listener_others.go
index bab11084..d86619b4 100644
--- a/pkg/portfwd/listener_others.go
+++ b/pkg/portfwd/listener_others.go
@@ -8,9 +8,25 @@ package portfwd
 import (
 	"context"
 	"net"
+	"path/filepath"
+
+	"github.com/sirupsen/logrus"
 )
 
 func Listen(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.Listener, error) {
+	if filepath.IsAbs(hostAddress) {
+		// Handle Unix domain sockets
+		if err := prepareUnixSocket(hostAddress); err != nil {
+			return nil, err
+		}
+		var lc net.ListenConfig
+		unixLis, err := lc.Listen(ctx, "unix", hostAddress)
+		if err != nil {
+			logrus.WithError(err).Errorf("failed to listen unix: %v", hostAddress)
+			return nil, err
+		}
+		return unixLis, nil
+	}
 	return listenConfig.Listen(ctx, "tcp", hostAddress)
 }
 
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index e9369203..8f85ef87 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -102,11 +102,10 @@ func registerExternalDriver(name, path string) {
 }
 
 func discoverDrivers() error {
-	prefix, err := usrlocalsharelima.Prefix()
+	stdDriverDir, err := usrlocalsharelima.LibexecLima()
 	if err != nil {
 		return err
 	}
-	stdDriverDir := filepath.Join(prefix, "libexec", "lima")
 
 	logrus.Debugf("Discovering external drivers in %s", stdDriverDir)
 	if _, err := os.Stat(stdDriverDir); err == nil {
diff --git a/pkg/usrlocalsharelima/usrlocalsharelima.go b/pkg/usrlocalsharelima/usrlocalsharelima.go
index cdd8ed93..d6daaa31 100644
--- a/pkg/usrlocalsharelima/usrlocalsharelima.go
+++ b/pkg/usrlocalsharelima/usrlocalsharelima.go
@@ -19,14 +19,14 @@ import (
 	"github.com/lima-vm/lima/v2/pkg/limatype"
 )
 
-// executableViaArgs0 returns the absolute path to the executable used to start this process.
+// ExecutableViaArgs0 returns the absolute path to the executable used to start this process.
 // It will also append the file extension on Windows, if necessary.
 // This function is different from os.Executable(), which will use /proc/self/exe on Linux
 // and therefore will resolve any symlink used to locate the executable. This function will
 // return the symlink instead because we want to be able to locate ../share/lima relative
 // to the location of the symlink, and not the actual executable. This is important when
 // using Homebrew.
-var executableViaArgs0 = sync.OnceValues(func() (string, error) {
+var ExecutableViaArgs0 = sync.OnceValues(func() (string, error) {
 	if os.Args[0] == "" {
 		return "", errors.New("os.Args[0] has not been set")
 	}
@@ -41,17 +41,17 @@ var executableViaArgs0 = sync.OnceValues(func() (string, error) {
 	return executable, nil
 })
 
-// Dir returns the location of the <PREFIX>/lima/share directory, relative to the location
-// of the current executable. It checks for multiple possible filesystem layouts and returns
-// the first candidate that contains the native guest agent binary.
-var Dir = sync.OnceValues(func() (string, error) {
-	selfPaths := []string{}
+// SelfDirs returns a list of directory paths where the current executable might be located.
+// It checks both os.Args[0] and os.Executable() methods and returns directories containing
+// the executable, resolving symlinks as needed.
+func SelfDirs() []string {
+	var selfPaths []string
 
-	selfViaArgs0, err := executableViaArgs0()
+	selfViaArgs0, err := ExecutableViaArgs0()
 	if err != nil {
 		logrus.WithError(err).Warn("failed to find executable from os.Args[0]")
 	} else {
-		selfPaths = append(selfPaths, selfViaArgs0)
+		selfPaths = append(selfPaths, filepath.Dir(selfViaArgs0))
 	}
 
 	selfViaOS, err := os.Executable()
@@ -64,11 +64,21 @@ var Dir = sync.OnceValues(func() (string, error) {
 			selfFinalPathViaOS = selfViaOS // fallback to the original path
 		}
 
-		if len(selfPaths) == 0 || selfFinalPathViaOS != selfPaths[0] {
-			selfPaths = append(selfPaths, selfFinalPathViaOS)
+		selfDir := filepath.Dir(selfFinalPathViaOS)
+		if len(selfPaths) == 0 || selfDir != selfPaths[0] {
+			selfPaths = append(selfPaths, selfDir)
 		}
 	}
 
+	return selfPaths
+}
+
+// Dir returns the location of the <PREFIX>/lima/share directory, relative to the location
+// of the current executable. It checks for multiple possible filesystem layouts and returns
+// the first candidate that contains the native guest agent binary.
+func Dir() (string, error) {
+	selfDirs := SelfDirs()
+
 	ostype := limatype.NewOS("linux")
 	arch := limatype.NewArch(runtime.GOARCH)
 	if arch == "" {
@@ -76,9 +86,8 @@ var Dir = sync.OnceValues(func() (string, error) {
 	}
 
 	gaCandidates := []string{}
-	for _, self := range selfPaths {
-		// self:  /usr/local/bin/limactl
-		selfDir := filepath.Dir(self)
+	for _, selfDir := range selfDirs {
+		// selfDir:  /usr/local/bin
 		selfDirDir := filepath.Dir(selfDir)
 		gaCandidates = append(gaCandidates,
 			// candidate 0:
@@ -118,8 +127,8 @@ var Dir = sync.OnceValues(func() (string, error) {
 	}
 
 	return "", fmt.Errorf("failed to find \"lima-guestagent.%s-%s\" binary for %v, attempted %v",
-		ostype, arch, selfPaths, gaCandidates)
-})
+		ostype, arch, selfDirs, gaCandidates)
+}
 
 // GuestAgentBinary returns the absolute path of the guest agent binary, possibly with ".gz" suffix.
 func GuestAgentBinary(ostype limatype.OS, arch limatype.Arch) (string, error) {
@@ -174,3 +183,12 @@ func Prefix() (string, error) {
 	}
 	return filepath.Dir(filepath.Dir(dir)), nil
 }
+
+// LibexecLima returns the <PREFIX>/libexec/lima directory.
+func LibexecLima() (string, error) {
+	prefix, err := Prefix()
+	if err != nil {
+		return "", err
+	}
+	return filepath.Join(prefix, "libexec", "lima"), nil
+}
diff --git a/website/content/en/docs/config/ai/_index.md b/website/content/en/docs/config/ai/_index.md
new file mode 100644
index 00000000..4d1a9c1f
--- /dev/null
+++ b/website/content/en/docs/config/ai/_index.md
@@ -0,0 +1,11 @@
+---
+title: AI
+weight: 90
+---
+
+There are two kinds of scenario to use Lima with AI:
+
+- [AI agents inside Lima](./inside): running an AI agent **inside** a VM
+- [AI agents outside Lima](./outside): calling Lima's MCP tools from an AI agent running **outside** a VM
+
+<!-- TODO: add a comparison table -->
\ No newline at end of file
diff --git a/website/content/en/docs/config/ai/inside/_index.md b/website/content/en/docs/config/ai/inside/_index.md
new file mode 100644
index 00000000..4356f5d0
--- /dev/null
+++ b/website/content/en/docs/config/ai/inside/_index.md
@@ -0,0 +1,19 @@
+---
+title: AI agents inside Lima
+weight: 10
+---
+
+Lima is useful for running AI agents (e.g., Codex, Claude, Gemini) so as to prevent them
+from directly reading, writing, or executing the host files.
+
+Lima v2.0 is planned to be released with built-in templates for well-known AI agents.
+
+For Lima v1.x, you can install AI agents in Lima manually.
+
+e.g.,
+
+```bash
+lima sudo apt install -y npm
+lima sudo npm install -g @google/gemini-cli
+lima gemini
+```
\ No newline at end of file
diff --git a/website/content/en/docs/config/ai/outside/_index.md b/website/content/en/docs/config/ai/outside/_index.md
new file mode 100644
index 00000000..7b0091aa
--- /dev/null
+++ b/website/content/en/docs/config/ai/outside/_index.md
@@ -0,0 +1,7 @@
+---
+title: AI agents outside Lima (MCP)
+weight: 10
+---
+
+Starting with Lima v2.0, Lima provides Model Context Protocol (MCP) tools
+for reading, writing, and executing local files using a VM sandbox.
\ No newline at end of file
diff --git a/website/content/en/docs/config/ai/outside/gemini.md b/website/content/en/docs/config/ai/outside/gemini.md
new file mode 100644
index 00000000..eef012dd
--- /dev/null
+++ b/website/content/en/docs/config/ai/outside/gemini.md
@@ -0,0 +1,60 @@
+---
+title: Gemini
+weight: 20
+
+---
+
+| ⚡ Requirement | Lima >= 2.0 |
+|---------------|-------------|
+
+This page describes how to use Lima as an sandbox for [Google Gemini CLI](https://github.com/google-gemini/gemini-cli).
+
+## Prerequisite
+In addition to Gemini and Lima, make sure that `limactl mcp` plugin is installed:
+
+```console
+$ limactl mcp -v
+limactl-mcp version 2.0.0-alpha.1
+```
+
+The `limactl mcp` plugin is bundled in Lima since v2.0, however, it may not be installed
+depending on the method of the [installation](../../../installation/).
+
+## Configuration
+1. Run the default Lima instance, with a mount of your project directory:
+```bash
+limactl start --mount-only "$(pwd):w" default
+```
+
+Drop the `:w` suffix if you do not want to allow writing to the mounted directory.
+
+2. Create `.gemini/extensions/lima/gemini-extension.json` as follows:
+```json
+{
+  "name": "lima",
+  "version": "2.0.0",
+  "mcpServers": {
+    "lima": {
+      "command": "limactl",
+      "args": [
+        "mcp",
+        "serve",
+        "default"
+      ]
+    }
+  }
+}
+```
+
+3. Modify `.gemini/settings.json` so as to disable Gemini CLI's [built-in tools](https://github.com/google-gemini/gemini-cli/tree/main/docs/tools)
+except ones that do not relate to local command execution and file I/O:
+```json
+{
+  "coreTools": ["WebFetchTool", "WebSearchTool", "MemoryTool"]
+}
+```
+
+## Usage
+Just run `gemini` in your project directory.
+
+Gemini automatically recognizes the MCP tools provided by Lima.
diff --git a/website/content/en/docs/config/environment-variables.md b/website/content/en/docs/config/environment-variables.md
index 69acb1e1..7881d5ed 100644
--- a/website/content/en/docs/config/environment-variables.md
+++ b/website/content/en/docs/config/environment-variables.md
@@ -106,6 +106,15 @@ This page documents the environment variables used in Lima.
   lima
   ```
 
+### `LIMA_SSH_OVER_VSOCK`
+- **Description**: Specifies to use vsock for SSH connection instead of port forwarding.
+- **Default**: `true` (since v2.0.0)
+- **Usage**: 
+  ```sh
+  export LIMA_SSH_OVER_VSOCK=true
+  ```
+- **Note**: This variable is effective only if the VM is VZ based and systemd is v256 or later (e.g. Ubuntu 24.10+).
+
 ### `LIMA_SSH_PORT_FORWARDER`
 
 - **Description**: Specifies to use the SSH port forwarder (slow) instead of gRPC (fast, previously unstable)
diff --git a/website/content/en/docs/config/plugin/cli.md b/website/content/en/docs/config/plugin/cli.md
index 78be0ef0..b2376aea 100644
--- a/website/content/en/docs/config/plugin/cli.md
+++ b/website/content/en/docs/config/plugin/cli.md
@@ -1,12 +1,71 @@
 ---
-title: CLI plugins
+title: CLI plugins (Experimental)
 weight: 2
 ---
 
  | ⚡ Requirement | Lima >= 2.0 |
  |----------------|-------------|
 
-Lima supports a plugin-like command aliasing system similar to `git`, `kubectl`, and `docker`. When you run a `limactl` command that doesn't exist, Lima will automatically look for an external program named `limactl-<command>` in your system's PATH.
+Lima supports a plugin-like command aliasing system similar to `git`, `kubectl`, and `docker`. When you run a `limactl` command that doesn't exist, Lima will automatically look for an external program named `limactl-<command>` in your system's PATH and additional directories.
+
+## Plugin Discovery
+
+Lima discovers plugins by scanning for executables named `limactl-<plugin-name>` in the following locations:
+
+1. **Directory containing the `limactl` binary** (including symlink support)
+2. **All directories in your `$PATH` environment variable**
+3. **`<PREFIX>/libexec/lima`** - For plugins installed by package managers or distribution packages
+
+Plugin discovery respects symlinks, ensuring that even if `limactl` is installed via Homebrew and points to a symlink, all plugins are correctly discovered.
+
+## Plugin Information
+
+Available plugins are automatically displayed in:
+
+- **`limactl --help`** - Shows all discovered plugins with descriptions in an "Available Plugins (Experimental)" section
+```bash
+Available Plugins (Experimental):
+  ps                  Sample limactl-ps alias that shows running instances
+  sh
+```
+
+
+- **`limactl info`** - Includes plugin information in the JSON output
+```json
+{
+   "plugins": [
+      {
+         "name": "ps",
+         "path": "/opt/homebrew/bin/limactl-ps"
+      },
+      {
+         "name": "sh",
+         "path": "/opt/homebrew/bin/limactl-sh"
+      }
+   ]
+}
+
+```
+
+### Plugin Descriptions
+
+Lima extracts plugin descriptions from script comments using the `<limactl-desc>` format. Include a description comment in your plugin script:
+
+```bash
+#!/bin/sh
+# <limactl-desc>Docker wrapper that connects to Docker daemon running in Lima instance</limactl-desc>
+set -eu
+
+# Rest of your script...
+```
+
+**Format Requirements:**
+- Only files beginning with a shebang (`#!`) are treated as scripts, and their `<limactl-desc>` lines will be extracted as the plugin description i.e Must contain exactly `<limactl-desc>Description text</limactl-desc>`
+- The description text should be concise and descriptive
+
+**Limitations:**
+- Binary executables cannot have descriptions extracted and will appear in the help output without a description
+- If no `<limactl-desc>` comment is found in a script, the plugin will appear in the help output without a description
 
 ## Creating Custom Aliases
 
@@ -45,16 +104,25 @@ After creating this alias:
 ```bash
 limactl sh default           # Equivalent to: limactl shell default
 limactl sh myinstance bash   # Equivalent to: limactl shell myinstance bash
-```
-
 ## How It Works
 
 1. When you run `limactl <unknown-command>`, Lima first tries to find a built-in command
-2. If no built-in command is found, Lima searches for `limactl-<unknown-command>` in your PATH
 3. If found, Lima executes the external program and passes all remaining arguments to it
 4. If not found, Lima shows the standard "unknown command" error
 
 This system allows you to:
 - Create personal shortcuts and aliases
 - Extend Lima's functionality without modifying the core application
-- Share custom commands with your team by distributing scripts
\ No newline at end of file
+- Share custom commands with your team by distributing scripts
+- Package plugins with Lima distributions in the `libexec/lima` directory
+
+## Package Installation
+
+Distribution packages and package managers can install plugins in `<PREFIX>/libexec/lima/` where `<PREFIX>` is typically `/usr/local` or `/opt/homebrew`. This allows plugins to be:
+- Managed by the package manager
+- Isolated from user's `$PATH`
+- Automatically discovered by Lima
+
+## Experimental Status
+
+**Experimental Feature**: The CLI plugin system is currently experimental and may change in future versions. Breaking changes to the plugin API or discovery mechanism may occur without notice.
diff --git a/website/content/en/docs/config/port.md b/website/content/en/docs/config/port.md
index 164e4b57..ee6a0b06 100644
--- a/website/content/en/docs/config/port.md
+++ b/website/content/en/docs/config/port.md
@@ -36,6 +36,20 @@ LIMA_SSH_PORT_FORWARDER=true limactl start
 - Doesn't support UDP based port forwarding
 - Spawns child process on host for running SSH master.
 
+#### SSH over AF_VSOCK
+
+| ⚡ Requirement | Lima >= 2.0 |
+|---------------|-------------|
+
+If VM is VZ based and systemd is v256 or later (e.g. Ubuntu 24.10+), Lima uses AF_VSOCK for communication between host and guest.
+SSH based port forwarding is much faster when using AF_VSOCK compared to traditional virtual network based port forwarding.
+
+To disable this feature, set `LIMA_SSH_OVER_VSOCK` to `false`:
+
+```bash
+export LIMA_SSH_OVER_VSOCK=false
+```
+
 ### Using GRPC
 
 | ⚡ Requirement | Lima >= 1.0 |
diff --git a/website/content/en/docs/releases/experimental.md b/website/content/en/docs/releases/experimental.md
index 0de26607..cfb7326d 100644
--- a/website/content/en/docs/releases/experimental.md
+++ b/website/content/en/docs/releases/experimental.md
@@ -19,6 +19,7 @@ The following commands are experimental and subject to change:
 - `limactl snapshot *`
 - `limactl tunnel`
 - `limactl template *`
+- `limactl mcp *`
 
 ## Graduated
 
diff --git a/website/hugo.toml b/website/hugo.toml
index 41b10795..b0748201 100644
--- a/website/hugo.toml
+++ b/website/hugo.toml
@@ -204,6 +204,10 @@ enable = false
     source="docsy"
     target="content/docs/Reference"
     language="en"
+  [[module.imports.mounts]]
+    source="docsy-mcp"
+    target="content/docs/config/ai/outside"
+    language="en"
   # Import templates README.md from templates folder
   [[module.imports]]
     path="../.."



Test patch:
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index aa9a8253..768650a5 100644
--- a/pkg/limayaml/defaults_test.go
+++ b/pkg/limayaml/defaults_test.go
@@ -268,6 +268,7 @@ func TestFillDefault(t *testing.T) {
 	expect.PortForwards[2].HostPort = 8888
 	expect.PortForwards[2].HostPortRange = [2]int{8888, 8888}
 
+	expect.PortForwards[3].HostPortRange = [2]int{0, 0}
 	expect.PortForwards[3].GuestSocket = fmt.Sprintf("%s | %s | %s | %s", user.HomeDir, user.Uid, user.Username, y.Param["ONE"])
 	expect.PortForwards[3].HostSocket = fmt.Sprintf("%s | %s | %s | %s | %s | %s", hostHome, instDir, instName, currentUser.Uid, currentUser.Username, y.Param["ONE"])