summary refs log tree commit diff
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2024-03-02 14:11:36 +0100
committerAlyssa Ross <hi@alyssa.is>2024-03-02 14:11:36 +0100
commitb287ea55210d103ea8798de7c303953027084b10 (patch)
treea04c3db9cda8232c1af0fd284c77334ce2a9a517
parent97dc7de1d4f8486f8538f4e32d5b472001ed4699 (diff)
downloadspectrum-portals.tar
spectrum-portals.tar.gz
spectrum-portals.tar.bz2
spectrum-portals.tar.lz
spectrum-portals.tar.xz
spectrum-portals.tar.zst
spectrum-portals.zip
-rw-r--r--.gitignore1
-rw-r--r--host/rootfs/Makefile9
-rw-r--r--host/rootfs/default.nix32
-rw-r--r--host/rootfs/etc/s6-linux-init/env/NO_AT_BRIDGE1
-rwxr-xr-xhost/rootfs/etc/s6-linux-init/run-image/service/vhost-user-fs/template/run5
-rw-r--r--host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/notification-fd1
-rw-r--r--host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/notification-fd.license2
-rwxr-xr-xhost/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/run5
-rw-r--r--host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd1
-rw-r--r--host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd.license2
-rwxr-xr-xhost/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/run16
-rw-r--r--host/rootfs/etc/s6-rc/ext-rc-init/up37
-rw-r--r--host/rootfs/etc/s6-rc/weston/run5
-rw-r--r--host/rootfs/etc/xdg/weston/weston.ini4
-rw-r--r--host/start-vmm/ch.rs11
-rw-r--r--host/start-vmm/lib.rs56
-rw-r--r--host/start-vmm/tests/meson.build8
-rw-r--r--host/start-vmm/tests/vm_command-basic.rs2
-rw-r--r--img/app/Makefile10
-rw-r--r--img/app/default.nix33
-rw-r--r--img/app/etc/dbus-1/session.conf61
-rw-r--r--img/app/etc/s6-rc/dbus/run4
-rw-r--r--img/app/etc/xdg/xdg-desktop-portal/portals.conf3
-rw-r--r--pkgs/default.nix4
-rw-r--r--tools/xdg-desktop-portal-spectrum-host/Cargo.lock1222
-rw-r--r--tools/xdg-desktop-portal-spectrum-host/Cargo.toml13
-rw-r--r--tools/xdg-desktop-portal-spectrum-host/default.nix14
-rw-r--r--tools/xdg-desktop-portal-spectrum-host/src/guest_dbus.rs132
-rw-r--r--tools/xdg-desktop-portal-spectrum-host/src/host_bus.rs100
-rw-r--r--tools/xdg-desktop-portal-spectrum-host/src/main.rs272
-rw-r--r--tools/xdg-desktop-portal-spectrum/default.nix21
-rw-r--r--tools/xdg-desktop-portal-spectrum/meson.build24
-rw-r--r--tools/xdg-desktop-portal-spectrum/org.freedesktop.impl.portal.desktop.spectrum.service.in3
-rw-r--r--tools/xdg-desktop-portal-spectrum/spectrum.portal3
-rw-r--r--tools/xdg-desktop-portal-spectrum/xdg-desktop-portal-spectrum.c125
35 files changed, 2181 insertions, 61 deletions
diff --git a/.gitignore b/.gitignore
index 0491ebb..8bb5039 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
 build/
 result
 result-*
+target/
 
 **/subprojects/*
 !**/subprojects/*.wrap
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile
index c5e467d..a8442a2 100644
--- a/host/rootfs/Makefile
+++ b/host/rootfs/Makefile
@@ -18,6 +18,7 @@ FILES = \
 	etc/mdev/wait \
 	etc/parse-devname \
 	etc/passwd \
+	etc/s6-linux-init/env/NO_AT_BRIDGE \
 	etc/s6-linux-init/run-image/service/getty-tty1/run \
 	etc/s6-linux-init/run-image/service/getty-tty2/run \
 	etc/s6-linux-init/run-image/service/getty-tty3/run \
@@ -35,6 +36,10 @@ FILES = \
 	etc/s6-linux-init/run-image/service/vmm/notification-fd \
 	etc/s6-linux-init/run-image/service/vmm/run \
 	etc/s6-linux-init/run-image/service/vmm/template/notification-fd \
+	etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/notification-fd \
+	etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/run \
+	etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd \
+	etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/run \
 	etc/s6-linux-init/scripts/rc.init \
 	etc/shared-dir \
 	etc/xdg/weston/autolaunch \
@@ -58,6 +63,10 @@ DIRS = \
 	etc/s6-linux-init/run-image/service/vmm/instances \
 	etc/s6-linux-init/run-image/service/vmm/template/data \
 	etc/s6-linux-init/run-image/service/vmm/template/env \
+	etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/instance \
+	etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/instances \
+	etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/data \
+	etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/env \
 	etc/s6-linux-init/run-image/vm \
 	ext \
 	run \
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix
index c6664bd..de2ae5a 100644
--- a/host/rootfs/default.nix
+++ b/host/rootfs/default.nix
@@ -3,13 +3,15 @@
 # SPDX-FileCopyrightText: 2022 Unikie
 
 import ../../lib/call-package.nix (
-{ callSpectrumPackage, lseek, src, pkgsMusl, pkgsStatic, linux_latest }:
+{ callSpectrumPackage, lseek, src, pkgsMusl, pkgsStatic, linux_latest, xdg-desktop-portal-gtk }:
 pkgsStatic.callPackage (
 
 { start-vmm
 , lib, stdenvNoCC, nixos, runCommand, writeReferencesToFile, erofs-utils, s6-rc
 , busybox, cloud-hypervisor, cryptsetup, execline, e2fsprogs, jq, kmod
 , mdevd, s6, s6-linux-init, socat, util-linuxMinimal, virtiofsd, xorg
+, xdg-desktop-portal-spectrum-host
+, strace
 }:
 
 let
@@ -19,11 +21,25 @@ let
   pkgsGui = pkgsMusl.extend (
     final: super:
     (optionalAttrs (systems.equals pkgsMusl.stdenv.hostPlatform super.stdenv.hostPlatform) {
+      appstream = super.appstream.override {
+        withSystemd = false;
+      };
+
       libgudev = super.libgudev.overrideAttrs ({ ... }: {
         # Tests use umockdev, which is not compatible with libudev-zero.
         doCheck = false;
       });
 
+      polkit = super.polkit.override {
+        useSystemd = false;
+      };
+      postgresql = super.postgresql.override {
+        enableSystemd = false;
+      };
+      procps = super.procps.override {
+        withSystemd = false;
+      };
+
       systemd = final.libudev-zero;
       systemdLibs = final.libudev-zero;
       systemdMinimal = final.libudev-zero;
@@ -32,6 +48,10 @@ let
         systemdSupport = false;
       };
 
+      util-linux = super.util-linux.override {
+        systemdSupport = false;
+      };
+
       weston = super.weston.overrideAttrs ({ mesonFlags ? [], ... }: {
         mesonFlags = mesonFlags ++ [
           "-Dsystemd=false"
@@ -45,6 +65,8 @@ let
   packages = [
     cloud-hypervisor e2fsprogs execline jq kmod mdevd
     s6 s6-linux-init s6-rc socat start-vmm virtiofsd
+    xdg-desktop-portal-spectrum-host
+    pkgsMusl.strace
 
     (cryptsetup.override {
       programs = {
@@ -91,8 +113,10 @@ let
   packagesSysroot = runCommand "packages-sysroot" {
     nativeBuildInputs = [ xorg.lndir ];
   } ''
-    mkdir -p $out/usr/bin
-    ln -s ${concatMapStringsSep " " (p: "${p}/bin/*") packages} $out/usr/bin
+    mkdir -p $out/usr/bin $out/usr/share
+    ln -st $out/usr/bin \
+        ${concatMapStringsSep " " (p: "${p}/bin/*") packages} \
+        ${xdg-desktop-portal-gtk}/libexec/xdg-desktop-portal-gtk
 
     for pkg in ${lib.escapeShellArgs usrPackages}; do
         lndir -ignorelinks -silent "$pkg" "$out/usr"
@@ -135,7 +159,7 @@ stdenvNoCC.mkDerivation {
 
   enableParallelBuilding = true;
 
-  passthru = { inherit appvm firmware kernel nixosAllHardware; };
+  passthru = { inherit appvm firmware kernel nixosAllHardware pkgsGui; };
 
   meta = with lib; {
     license = licenses.eupl12;
diff --git a/host/rootfs/etc/s6-linux-init/env/NO_AT_BRIDGE b/host/rootfs/etc/s6-linux-init/env/NO_AT_BRIDGE
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/host/rootfs/etc/s6-linux-init/env/NO_AT_BRIDGE
@@ -0,0 +1 @@
+1
diff --git a/host/rootfs/etc/s6-linux-init/run-image/service/vhost-user-fs/template/run b/host/rootfs/etc/s6-linux-init/run-image/service/vhost-user-fs/template/run
index c055cc0..a6233b6 100755
--- a/host/rootfs/etc/s6-linux-init/run-image/service/vhost-user-fs/template/run
+++ b/host/rootfs/etc/s6-linux-init/run-image/service/vhost-user-fs/template/run
@@ -8,5 +8,6 @@ if { fdmove 1 3 echo }
 fdclose 3
 
 export TMPDIR /run
-backtick -E shared_dir { /etc/shared-dir -v servicename=${1} }
-virtiofsd --fd 0 --shared-dir $shared_dir
+# backtick -E shared_dir { /etc/shared-dir -v servicename=${1} }
+define shared_dir /run/vm/${1}/fs
+virtiofsd --fd 0 --shared-dir $shared_dir --sandbox none
diff --git a/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/notification-fd b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/notification-fd
new file mode 100644
index 0000000..00750ed
--- /dev/null
+++ b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/notification-fd
@@ -0,0 +1 @@
+3
diff --git a/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/notification-fd.license b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/notification-fd.license
new file mode 100644
index 0000000..a941ca4
--- /dev/null
+++ b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/notification-fd.license
@@ -0,0 +1,2 @@
+SPDX-License-Identifier: CC0-1.0
+SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is>
diff --git a/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/run b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/run
new file mode 100755
index 0000000..9041788
--- /dev/null
+++ b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/run
@@ -0,0 +1,5 @@
+#!/bin/execlineb -P
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is>
+
+s6-svscan -d3 instance
diff --git a/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd
new file mode 100644
index 0000000..00750ed
--- /dev/null
+++ b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd
@@ -0,0 +1 @@
+3
diff --git a/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd.license b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd.license
new file mode 100644
index 0000000..5a40633
--- /dev/null
+++ b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/notification-fd.license
@@ -0,0 +1,2 @@
+SPDX-License-Identifier: CC0-1.0
+SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is>
diff --git a/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/run b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/run
new file mode 100755
index 0000000..d45fa6d
--- /dev/null
+++ b/host/rootfs/etc/s6-linux-init/run-image/service/xdg-desktop-portal-spectrum-host/template/run
@@ -0,0 +1,16 @@
+#!/bin/execlineb -S1
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2024 Alyssa Ross <hi@alyssa.is>
+
+export XDG_RUNTIME_DIR /run/user/0
+export WAYLAND_DISPLAY wayland-1
+export XDG_DESKTOP_PORTAL_SPECTRUM_HOST_FS_ROOT /run/vm/${1}/fs
+
+if { mkdir -p /run/service/vmm/instance/${1}/env }
+
+s6-ipcserver-socketbinder -a 0700 /run/service/vmm/instance/${1}/env/vsock.sock_219
+
+if { fdmove 1 3 echo }
+fdclose 3
+
+xdg-desktop-portal-spectrum-host
diff --git a/host/rootfs/etc/s6-rc/ext-rc-init/up b/host/rootfs/etc/s6-rc/ext-rc-init/up
index 0214759..2532bbe 100644
--- a/host/rootfs/etc/s6-rc/ext-rc-init/up
+++ b/host/rootfs/etc/s6-rc/ext-rc-init/up
@@ -1,5 +1,5 @@
 # SPDX-License-Identifier: EUPL-1.2+
-# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is>
 # SPDX-FileCopyrightText: 2022 Unikie
 
 cd /ext/svc/data
@@ -11,16 +11,25 @@ if {
   if { mkdir /run/vm/${name} }
   if { ln -s /ext/svc/data/${name} /run/vm/${name}/config }
 
+  # Sockets for vhost-user backends need to be created before the VMM
+  # is started, because it will try to connect to them.
+
+  # if {
+  #   if -t { test -e ${name}/shared-dirs }
+  #   cd ${name}/shared-dirs
+  #   elglob -0 fsnames *
+
+  #   if {
+  #     forx -po0 -E fsname { $fsnames }
+  #     s6-instance-create /run/service/vhost-user-fs ${name}:${fsname}
+  #   }
+  #   s6-svwait -U /run/service/vhost-user-fs/instance/${name}:${fsnames}
+  # }
+
   if {
-    if -t { test -e ${name}/shared-dirs }
-    cd ${name}/shared-dirs
-    elglob -0 fsnames *
-
-    if {
-      forx -po0 -E fsname { $fsnames }
-      s6-instance-create /run/service/vhost-user-fs ${name}:${fsname}
-    }
-    s6-svwait -U /run/service/vhost-user-fs/instance/${name}:${fsnames}
+    if { mkdir /run/vm/${name}/fs }
+    if { s6-instance-create /run/service/vhost-user-fs $name }
+    s6-svwait -U /run/service/vhost-user-fs/instance/${name}
   }
 
   if {
@@ -29,7 +38,13 @@ if {
     s6-svwait -U /run/service/vhost-user-gpu/instance/${name}
   }
 
-  s6-instance-create /run/service/vmm $name
+  if { s6-instance-create /run/service/vmm $name }
+
+  # The service directory for the VMM needs to exist before
+  # xdg-desktop-portal-spectrum-host is started, so it can install its
+  # listening vsock socket.
+  if { s6-instance-create /run/service/xdg-desktop-portal-spectrum-host $name }
+  s6-svwait -U /run/service/xdg-desktop-portal-spectrum-host/instance/${name}
 }
 
 s6-svwait -U /run/service/vmm/instance/${names}
diff --git a/host/rootfs/etc/s6-rc/weston/run b/host/rootfs/etc/s6-rc/weston/run
index df2d74e..309eb0f 100644
--- a/host/rootfs/etc/s6-rc/weston/run
+++ b/host/rootfs/etc/s6-rc/weston/run
@@ -10,10 +10,7 @@ foreground {
 unexport ?
 
 backtick USER { id -un }
-backtick HOME {
-  importas -i user USER
-  homeof $user
-}
+export HOME /run
 
 export XDG_RUNTIME_DIR /run/user/0
 redirfd -r 0 /dev/tty1
diff --git a/host/rootfs/etc/xdg/weston/weston.ini b/host/rootfs/etc/xdg/weston/weston.ini
index cdf8666..5595554 100644
--- a/host/rootfs/etc/xdg/weston/weston.ini
+++ b/host/rootfs/etc/xdg/weston/weston.ini
@@ -3,3 +3,7 @@
 
 [autolaunch]
 path=/etc/xdg/weston/autolaunch
+
+[output]
+name=Virtual-1
+mode=1920x1440
diff --git a/host/start-vmm/ch.rs b/host/start-vmm/ch.rs
index cc05d84..9fa58a3 100644
--- a/host/start-vmm/ch.rs
+++ b/host/start-vmm/ch.rs
@@ -33,7 +33,7 @@ pub struct DiskConfig {
 #[derive(Serialize)]
 pub struct FsConfig {
     pub socket: String,
-    pub tag: String,
+    pub tag: &'static str,
 }
 
 #[derive(Serialize)]
@@ -61,15 +61,22 @@ pub struct PayloadConfig {
 }
 
 #[derive(Serialize)]
+pub struct VsockConfig {
+    pub cid: u32,
+    pub socket: &'static str,
+}
+
+#[derive(Serialize)]
 pub struct VmConfig {
     pub console: ConsoleConfig,
     pub disks: Vec<DiskConfig>,
-    pub fs: Vec<FsConfig>,
+    pub fs: [FsConfig; 1],
     pub gpu: Vec<GpuConfig>,
     pub memory: MemoryConfig,
     pub net: Vec<NetConfig>,
     pub payload: PayloadConfig,
     pub serial: ConsoleConfig,
+    pub vsock: VsockConfig,
 }
 
 fn command(vm_name: &str, s: impl AsRef<OsStr>) -> Command {
diff --git a/host/start-vmm/lib.rs b/host/start-vmm/lib.rs
index 837e3d5..98dfaf5 100644
--- a/host/start-vmm/lib.rs
+++ b/host/start-vmm/lib.rs
@@ -18,7 +18,7 @@ use std::os::unix::process::parent_id;
 use std::path::Path;
 use std::process::{exit, Command};
 
-use ch::{ConsoleConfig, DiskConfig, FsConfig, GpuConfig, MemoryConfig, PayloadConfig, VmConfig};
+use ch::{ConsoleConfig, DiskConfig, FsConfig, GpuConfig, MemoryConfig, PayloadConfig, VmConfig, VsockConfig};
 use fork::double_fork;
 use net::net_setup;
 use s6::notify_readiness;
@@ -65,7 +65,7 @@ pub fn vm_config(vm_name: &str, config_root: &Path) -> Result<VmConfig, String>
     let blk_dir = config_dir.join("blk");
     let kernel_path = config_dir.join("vmlinux");
     let net_providers_dir = config_dir.join("providers/net");
-    let shared_dirs_dir = config_dir.join("shared-dirs");
+    // let shared_dirs_dir = config_dir.join("shared-dirs");
     let wayland_path = config_dir.join("wayland");
 
     Ok(VmConfig {
@@ -102,27 +102,33 @@ pub fn vm_config(vm_name: &str, config_root: &Path) -> Result<VmConfig, String>
                 .collect::<Result<_, _>>()?,
             Err(e) => return Err(format!("reading directory {:?}: {}", blk_dir, e)),
         },
-        fs: match shared_dirs_dir.read_dir() {
-            Ok(entries) => entries
-                .into_iter()
-                .map(|result| {
-                    let entry = result
-                        .map_err(|e| format!("examining directory entry: {}", e))?
-                        .file_name();
-
-                    let entry = entry.to_str().ok_or_else(|| {
-                        format!("shared directory name {:?} is not valid UTF-8", entry)
-                    })?;
-
-                    Ok(FsConfig {
-                        tag: entry.to_string(),
-                        socket: format!("/run/service/vhost-user-fs/instance/{vm_name}:{entry}/env/virtiofsd.sock"),
-                    })
-                })
-                .collect::<Result<_, String>>()?,
-            Err(e) if e.kind() == ErrorKind::NotFound => Default::default(),
-            Err(e) => return Err(format!("reading directory {:?}: {e}", shared_dirs_dir)),
-        },
+        // fs: match shared_dirs_dir.read_dir() {
+        //     Ok(entries) => entries
+        //         .into_iter()
+        //         .map(|result| {
+        //             let entry = result
+        //                 .map_err(|e| format!("examining directory entry: {}", e))?
+        //                 .file_name();
+
+        //             let entry = entry.to_str().ok_or_else(|| {
+        //                 format!("shared directory name {:?} is not valid UTF-8", entry)
+        //             })?;
+
+        //             Ok(FsConfig {
+        //                 tag: entry.to_string(),
+        //                 socket: format!("/run/service/vhost-user-fs/instance/{vm_name}:{entry}/env/virtiofsd.sock"),
+        //             })
+        //         })
+        //         .collect::<Result<_, String>>()?,
+        //     Err(e) if e.kind() == ErrorKind::NotFound => Default::default(),
+        //     Err(e) => return Err(format!("reading directory {:?}: {e}", shared_dirs_dir)),
+        // },
+        fs: [
+            FsConfig {
+                tag: "virtiofs0",
+                socket: format!("/run/service/vhost-user-fs/instance/{vm_name}/env/virtiofsd.sock"),
+            }
+        ],
         gpu: match wayland_path.try_exists() {
             Ok(true) => vec![GpuConfig {
                 socket: format!("/run/service/vhost-user-gpu/instance/{vm_name}/env/crosvm.sock"),
@@ -170,6 +176,10 @@ pub fn vm_config(vm_name: &str, config_root: &Path) -> Result<VmConfig, String>
             mode: "File",
             file: Some(format!("/run/{vm_name}.log")),
         },
+        vsock: VsockConfig {
+            cid: 3,
+            socket: "env/vsock.sock",
+        },
     })
 }
 
diff --git a/host/start-vmm/tests/meson.build b/host/start-vmm/tests/meson.build
index 7f6bd08..1b4161f 100644
--- a/host/start-vmm/tests/meson.build
+++ b/host/start-vmm/tests/meson.build
@@ -37,7 +37,7 @@ test('vm_command-multiple-disks', executable('vm_command-multiple-disks',
   'vm_command-multiple-disks.rs',
   dependencies : rust_lib_dep,
   link_with : rust_helper))
-test('vm_command-shared-dir', executable('vm_command-shared-dir',
-  'vm_command-shared-dir.rs',
-  dependencies : rust_lib_dep,
-  link_with : rust_helper))
+# test('vm_command-shared-dir', executable('vm_command-shared-dir',
+#   'vm_command-shared-dir.rs',
+#   dependencies : rust_lib_dep,
+#   link_with : rust_helper))
diff --git a/host/start-vmm/tests/vm_command-basic.rs b/host/start-vmm/tests/vm_command-basic.rs
index 305e98c..0a533b8 100644
--- a/host/start-vmm/tests/vm_command-basic.rs
+++ b/host/start-vmm/tests/vm_command-basic.rs
@@ -31,6 +31,8 @@ fn main() -> std::io::Result<()> {
     assert!(config.memory.shared);
     assert_eq!(config.serial.mode, "File");
     assert_eq!(config.serial.file.unwrap(), "/run/testvm.log");
+    assert_eq!(config.vsock.cid, 3);
+    assert_eq!(config.vsock.socket, "env/vsock.sock");
 
     Ok(())
 }
diff --git a/img/app/Makefile b/img/app/Makefile
index 38df7d0..43751d1 100644
--- a/img/app/Makefile
+++ b/img/app/Makefile
@@ -27,6 +27,7 @@ $(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-fie
 	mv $@.tmp $@
 
 VM_FILES = \
+	etc/dbus-1/session.conf \
 	etc/fstab \
 	etc/init \
 	etc/mdev.conf \
@@ -39,7 +40,8 @@ VM_FILES = \
 	etc/s6-linux-init/env/DBUS_SESSION_BUS_ADDRESS \
 	etc/s6-linux-init/env/NIX_XDG_DESKTOP_PORTAL_DIR \
 	etc/s6-linux-init/env/XDG_RUNTIME_DIR \
-	etc/s6-linux-init/scripts/rc.init
+	etc/s6-linux-init/scripts/rc.init \
+	etc/xdg/xdg-desktop-portal/portals.conf
 VM_DIRS = dev run proc sys \
 	etc/s6-linux-init/env \
 	etc/s6-linux-init/run-image/ext \
@@ -125,13 +127,14 @@ run-qemu: $(imgdir)/appvm/blk/root.img start-virtiofsd
 	    -device virtio-gpu-rutabaga-pci,cross-domain=on,hostmem=8G \
 	    -object memory-backend-memfd,id=mem,size=256M,share=on \
 	    -numa node,memdev=mem \
+	    -device vhost-vsock-pci,guest-cid=3 \
 	    -chardev vc,id=virtiocon0 \
 	    -device virtio-serial-pci \
 	    -device virtconsole,chardev=virtiocon0
 .PHONY: run-qemu
 
 run-cloud-hypervisor: $(imgdir)/appvm/blk/root.img start-vhost-user-gpu start-virtiofsd
-	rm -f build/vmm.sock
+	rm -f build/vmm.sock build/vsock.sock
 	@../../scripts/with-taps.elb ../../scripts/run-cloud-hypervisor.sh \
 	    --api-socket path=build/vmm.sock \
 	    --memory size=256M,shared=on \
@@ -139,10 +142,11 @@ run-cloud-hypervisor: $(imgdir)/appvm/blk/root.img start-vhost-user-gpu start-vi
 	           path=$(RUN_IMG),readonly=on \
 	    --fs tag=virtiofs0,socket=build/virtiofsd.sock \
 	    --gpu socket=build/vhost-user-gpu.sock \
+	    --vsock cid=3,socket=build/vsock.sock \
 	    --net tap=tap0 \
 	    --kernel $(KERNEL) \
 	    --cmdline "root=PARTLABEL=root" \
-	    --console tty \
+	    --console pty \
 	    --serial file=build/serial.log
 .PHONY: run-cloud-hypervisor
 
diff --git a/img/app/default.nix b/img/app/default.nix
index 1cc4719..220d2dd 100644
--- a/img/app/default.nix
+++ b/img/app/default.nix
@@ -1,13 +1,19 @@
 # SPDX-License-Identifier: MIT
-# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is>
 
 import ../../lib/call-package.nix (
-{ lseek, src, terminfo, pkgsMusl, pkgsStatic }:
+{ lseek, src
+, terminfo, pkgsMusl, pkgsStatic
+, systemd
+}:
+
 pkgsStatic.callPackage (
 
 { lib, stdenvNoCC, runCommand, writeReferencesToFile
 , erofs-utils, jq, s6-rc, util-linux
 , busybox, cacert, dbus, execline, kmod, linux_latest, mdevd, s6, s6-linux-init
+, xdg-desktop-portal-spectrum
+, socat
 }:
 
 let
@@ -15,6 +21,8 @@ let
 
   packages = [
     dbus execline kmod mdevd s6 s6-linux-init s6-rc
+    xdg-desktop-portal-spectrum
+    pkgsMusl.strace socat
 
     (busybox.override {
       extraConfig = ''
@@ -37,13 +45,20 @@ let
     inherit packages;
     passAsFile = [ "packages" ];
   } ''
-    mkdir -p $out/usr/bin $out/usr/share/dbus-1/services
+    mkdir -p \
+        $out/usr/bin \
+        $out/usr/share/dbus-1/services \
+        $out/usr/share/xdg-desktop-portal/portals
     ln -s ${concatMapStringsSep " " (p: "${p}/bin/*") packages} $out/usr/bin
+    ln -s ${systemd}/bin/busctl $out/usr/bin
     ln -s ${dbus}/share/dbus-1/session.conf $out/usr/share/dbus-1
     ln -st $out/usr/share/dbus-1/services \
         ${pkgsMusl.xdg-desktop-portal}/share/dbus-1/services/*.service \
-        ${pkgsMusl.xdg-desktop-portal-gtk}/share/dbus-1/services/*.service
-    ln -s ${pkgsMusl.xdg-desktop-portal-gtk}/share/xdg-desktop-portal $out/usr/share
+        ${pkgsMusl.xdg-desktop-portal-gtk}/share/dbus-1/services/*.service \
+        ${xdg-desktop-portal-spectrum}/share/dbus-1/services/*.service        
+    ln -st $out/usr/share/xdg-desktop-portal/portals \
+        ${pkgsMusl.xdg-desktop-portal-gtk}/share/xdg-desktop-portal/portals/*.portal \
+        ${xdg-desktop-portal-spectrum}/share/xdg-desktop-portal/portals/*.portal
     ln -s ${kernel}/lib "$out"
     ln -s ${terminfo}/share/terminfo $out/usr/share
     ln -s ${cacert}/etc/ssl $out/usr/share
@@ -78,6 +93,14 @@ let
       VIRTIO_CONSOLE = yes;
       VIRTIO_PCI = yes;
       VT = no;
+
+      VSOCKETS = yes;
+      VIRTIO_VSOCKETS_COMMON = yes;
+      VIRTIO_VSOCKETS = yes;
+
+      AGP = no;
+      DRM_VIRTIO_GPU = yes;
+      DRM = yes;
     };
   }).overrideAttrs ({ installFlags ? [], ... }: {
     installFlags = installFlags ++ [
diff --git a/img/app/etc/dbus-1/session.conf b/img/app/etc/dbus-1/session.conf
new file mode 100644
index 0000000..d41f5a6
--- /dev/null
+++ b/img/app/etc/dbus-1/session.conf
@@ -0,0 +1,61 @@
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+  <listen>vsock:</listen>
+  <listen>unix:path=/run/session-bus</listen>
+
+  <auth>ANONYMOUS</auth>
+  <auth>EXTERNAL</auth>
+  <allow_anonymous />
+
+  <standard_session_servicedirs />
+
+  <policy context="default">
+    <!-- Allow everything to be sent -->
+    <allow send_destination="*" eavesdrop="true"/>
+    <!-- Allow everything to be received -->
+    <allow eavesdrop="true"/>
+    <!-- Allow anyone to own anything -->
+    <allow own="*"/>
+  </policy>
+
+  <!-- Config files are placed here that among other things, 
+       further restrict the above policy for specific services. -->
+  <includedir>session.d</includedir>
+
+  <includedir>/etc/dbus-1/session.d</includedir>
+
+  <!-- This is included last so local configuration can override what's 
+       in this standard file -->
+  <include ignore_missing="yes">/etc/dbus-1/session-local.conf</include>
+
+  <include if_selinux_enabled="yes" selinux_root_relative="yes">contexts/dbus_contexts</include>
+
+  <!-- For the session bus, override the default relatively-low limits 
+       with essentially infinite limits, since the bus is just running 
+       as the user anyway, using up bus resources is not something we need 
+       to worry about. In some cases, we do set the limits lower than 
+       "all available memory" if exceeding the limit is almost certainly a bug, 
+       having the bus enforce a limit is nicer than a huge memory leak. But the 
+       intent is that these limits should never be hit. -->
+
+  <!-- the memory limits are 1G instead of say 4G because they can't exceed 32-bit signed int max -->
+  <limit name="max_incoming_bytes">1000000000</limit>
+  <limit name="max_incoming_unix_fds">250000000</limit>
+  <limit name="max_outgoing_bytes">1000000000</limit>
+  <limit name="max_outgoing_unix_fds">250000000</limit>
+  <limit name="max_message_size">1000000000</limit>
+  <!-- We do not override max_message_unix_fds here since the in-kernel
+       limit is also relatively low -->
+  <limit name="service_start_timeout">120000</limit>  
+  <limit name="auth_timeout">240000</limit>
+  <limit name="pending_fd_timeout">150000</limit>
+  <limit name="max_completed_connections">100000</limit>  
+  <limit name="max_incomplete_connections">10000</limit>
+  <limit name="max_connections_per_user">100000</limit>
+  <limit name="max_pending_service_starts">10000</limit>
+  <limit name="max_names_per_connection">50000</limit>
+  <limit name="max_match_rules_per_connection">50000</limit>
+  <limit name="max_replies_per_connection">50000</limit>
+
+</busconfig>
diff --git a/img/app/etc/s6-rc/dbus/run b/img/app/etc/s6-rc/dbus/run
index 7c94772..53db196 100644
--- a/img/app/etc/s6-rc/dbus/run
+++ b/img/app/etc/s6-rc/dbus/run
@@ -2,10 +2,8 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is>
 
-importas -i address DBUS_SESSION_BUS_ADDRESS
 
 dbus-daemon
-  --address $address
-  --config-file /usr/share/dbus-1/session.conf
+  --config-file /etc/dbus-1/session.conf
   --nofork
   --print-address 3
diff --git a/img/app/etc/xdg/xdg-desktop-portal/portals.conf b/img/app/etc/xdg/xdg-desktop-portal/portals.conf
new file mode 100644
index 0000000..9dce6ef
--- /dev/null
+++ b/img/app/etc/xdg/xdg-desktop-portal/portals.conf
@@ -0,0 +1,3 @@
+[preferred]
+default=gtk
+org.freedesktop.impl.portal.FileChooser=spectrum
diff --git a/pkgs/default.nix b/pkgs/default.nix
index 850ac6a..032dbf9 100644
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -39,6 +39,10 @@ let
     rootfs = self.callSpectrumPackage ../host/rootfs {};
     start-vmm = self.callSpectrumPackage ../host/start-vmm {};
     run-spectrum-vm = self.callSpectrumPackage ../scripts/run-spectrum-vm.nix {};
+    xdg-desktop-portal-spectrum =
+      self.callSpectrumPackage ../tools/xdg-desktop-portal-spectrum {};
+    xdg-desktop-portal-spectrum-host =
+      self.callSpectrumPackage ../tools/xdg-desktop-portal-spectrum-host {};
 
     # Packages from the overlay, so it's possible to build them from
     # the CLI easily.
diff --git a/tools/xdg-desktop-portal-spectrum-host/Cargo.lock b/tools/xdg-desktop-portal-spectrum-host/Cargo.lock
new file mode 100644
index 0000000..1958fc5
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum-host/Cargo.lock
@@ -0,0 +1,1222 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb"
+dependencies = [
+ "event-listener 5.1.0",
+ "event-listener-strategy 0.5.0",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 5.1.0",
+ "event-listener-strategy 0.5.0",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c"
+dependencies = [
+ "async-lock 3.3.0",
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1"
+dependencies = [
+ "async-lock 3.3.0",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65"
+dependencies = [
+ "async-lock 3.3.0",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
+dependencies = [
+ "event-listener 2.5.3",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b"
+dependencies = [
+ "event-listener 4.0.3",
+ "event-listener-strategy 0.4.0",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "451e3cf68011bd56771c79db04a9e333095ab6349f7e47592b788e9b98720cc8"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock 3.3.0",
+ "async-signal",
+ "blocking",
+ "cfg-if",
+ "event-listener 5.1.0",
+ "futures-lite",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
+dependencies = [
+ "async-io",
+ "async-lock 2.8.0",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
+
+[[package]]
+name = "async-trait"
+version = "0.1.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
+dependencies = [
+ "async-channel",
+ "async-lock 3.3.0",
+ "async-task",
+ "fastrand",
+ "futures-io",
+ "futures-lite",
+ "piper",
+ "tracing",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "endi"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "event-listener"
+version = "4.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7ad6fd685ce13acd6d9541a30f6db6567a7a24c9ffd4ba2955d29e3f22c8b27"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3"
+dependencies = [
+ "event-listener 4.0.3",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291"
+dependencies = [
+ "event-listener 5.1.0",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-lite"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+
+[[package]]
+name = "memchr"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "nix"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "polling"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "pin-project-lite",
+ "rustix",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
+dependencies = [
+ "toml_edit 0.21.1",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+
+[[package]]
+name = "rustix"
+version = "0.38.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "uds_windows"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "winapi",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.3",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.3",
+ "windows_aarch64_msvc 0.52.3",
+ "windows_i686_gnu 0.52.3",
+ "windows_i686_msvc 0.52.3",
+ "windows_x86_64_gnu 0.52.3",
+ "windows_x86_64_gnullvm 0.52.3",
+ "windows_x86_64_msvc 0.52.3",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "xdg-desktop-portal-spectrum-host"
+version = "0.1.0"
+dependencies = [
+ "async-executor",
+ "async-io",
+ "futures-lite",
+ "libc",
+ "percent-encoding",
+ "url",
+ "zbus",
+]
+
+[[package]]
+name = "xdg-home"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "zbus"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93b5c7bc69c56e09f4e379e250ae9028192e82aeaf7b9f8e72a86a2238a26d4c"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-fs",
+ "async-io",
+ "async-lock 3.3.0",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "derivative",
+ "enumflags2",
+ "event-listener 5.1.0",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "hex",
+ "nix",
+ "ordered-stream",
+ "rand",
+ "serde",
+ "serde_repr",
+ "sha1",
+ "static_assertions",
+ "tracing",
+ "uds_windows",
+ "windows-sys 0.52.0",
+ "xdg-home",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "067cc3a28ae806573727b8206a3a6a4718da55c54b2f70620a48fa92dd3bbc7d"
+dependencies = [
+ "proc-macro-crate 3.1.0",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 1.0.109",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
+dependencies = [
+ "serde",
+ "static_assertions",
+ "zvariant",
+]
+
+[[package]]
+name = "zvariant"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e09e8be97d44eeab994d752f341e67b3b0d80512a8b315a0671d47232ef1b65"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "static_assertions",
+ "zvariant_derive",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72a5857e2856435331636a9fbb415b09243df4521a267c5bedcd5289b4d5799e"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
diff --git a/tools/xdg-desktop-portal-spectrum-host/Cargo.toml b/tools/xdg-desktop-portal-spectrum-host/Cargo.toml
new file mode 100644
index 0000000..aa41bf3
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum-host/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "xdg-desktop-portal-spectrum-host"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+async-executor = "1.8.0"
+async-io = "2.3.1"
+futures-lite = "2.2.0"
+libc = "0.2.153"
+percent-encoding = "2.3.1"
+url = "2.5.0"
+zbus = { version = "4.1.0", features = ["p2p"] }
diff --git a/tools/xdg-desktop-portal-spectrum-host/default.nix b/tools/xdg-desktop-portal-spectrum-host/default.nix
new file mode 100644
index 0000000..c731bb4
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum-host/default.nix
@@ -0,0 +1,14 @@
+import ../../lib/call-package.nix (
+{ src, lib, rustPlatform }:
+
+rustPlatform.buildRustPackage {
+  name = "xdg-desktop-portal-spectrum-host";
+
+  src = lib.fileset.toSource {
+    root = ../..;
+    fileset = lib.fileset.intersection src ./.;
+  };
+  sourceRoot = "source/tools/xdg-desktop-portal-spectrum-host";
+
+  cargoLock.lockFile = ./Cargo.lock;
+}) (_: {})
diff --git a/tools/xdg-desktop-portal-spectrum-host/src/guest_dbus.rs b/tools/xdg-desktop-portal-spectrum-host/src/guest_dbus.rs
new file mode 100644
index 0000000..a1a9e34
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum-host/src/guest_dbus.rs
@@ -0,0 +1,132 @@
+use std::collections::BTreeMap;
+use std::ffi::OsString;
+use std::os::unix::prelude::*;
+use std::path::PathBuf;
+
+use percent_encoding::percent_decode;
+use url::Url;
+use zbus::zvariant::{Array, ObjectPath, OwnedValue, Value};
+use zbus::{interface, proxy};
+
+use crate::{share_file, FILE_CHOOSER_PROXY};
+
+#[proxy(
+    assume_defaults = false,
+    default_path = "/org/freedesktop/portal/desktop",
+    interface = "org.freedesktop.impl.portal.FileChooser"
+)]
+trait FileChooser {
+    fn open_file(
+        &self,
+        handle: &ObjectPath<'_>,
+        app_id: &str,
+        parent_window: &str,
+        title: &str,
+        options: BTreeMap<&str, Value<'_>>,
+    ) -> zbus::fdo::Result<(u32, BTreeMap<String, OwnedValue>)>;
+
+    fn save_file(
+        &self,
+        handle: &ObjectPath<'_>,
+        app_id: &str,
+        parent_window: &str,
+        title: &str,
+        options: BTreeMap<&str, Value<'_>>,
+    ) -> zbus::fdo::Result<(u32, BTreeMap<String, OwnedValue>)>;
+
+    fn save_files(
+        &self,
+        handle: &ObjectPath<'_>,
+        app_id: &str,
+        parent_window: &str,
+        title: &str,
+        options: BTreeMap<&str, Value<'_>>,
+    ) -> zbus::fdo::Result<(u32, BTreeMap<String, OwnedValue>)>;
+}
+
+#[derive(Debug)]
+pub struct FileChooserImpl;
+
+#[interface(name = "org.freedesktop.impl.portal.FileChooser")]
+impl FileChooserImpl {
+    async fn open_file(
+        &self,
+        handle: ObjectPath<'_>,
+        app_id: &str,
+        parent_window: &str,
+        title: &str,
+        options: BTreeMap<&str, Value<'_>>,
+    ) -> zbus::fdo::Result<(u32, BTreeMap<String, OwnedValue>)> {
+        let (response, mut results) = FILE_CHOOSER_PROXY
+            .get()
+            .unwrap()
+            .open_file(&handle, app_id, parent_window, title, options)
+            .await?;
+
+        eprintln!("results keys: {:?}", results.keys());
+
+        let writable = results
+            .get("writable")
+            .unwrap_or(&OwnedValue::from(false))
+            .downcast_ref::<bool>()
+            .expect("writeable is not a bool");
+
+        if let Some(uris) = results.get("uris") {
+            let uris = uris.downcast_ref::<Array>().expect("uris is not an array");
+            let mut uris: Vec<String> = uris.try_into().expect("uri is not a string");
+
+            for uri in uris.iter_mut() {
+                let path = uri.strip_prefix("file://").expect("uri is not a file:// URL");
+                let path = percent_decode(path.as_bytes());
+                let path = PathBuf::from(OsString::from_vec(path.collect()));
+                    
+                share_file(path.clone(), writable);
+
+                let mut guest_path = PathBuf::from("/run/virtiofs/virtiofs0");
+                guest_path.push(path.file_name().expect("path has no file name"));
+                *uri = Url::from_file_path(guest_path).unwrap().to_string();
+            }
+
+            let uris: Array = uris.into();
+            results.insert("uris".to_string(), OwnedValue::try_from(uris).unwrap());
+        }
+
+        Ok((response, results))
+    }
+
+    async fn save_file(
+        &self,
+        handle: ObjectPath<'_>,
+        app_id: &str,
+        parent_window: &str,
+        title: &str,
+        options: BTreeMap<&str, Value<'_>>,
+    ) -> zbus::fdo::Result<(u32, BTreeMap<String, OwnedValue>)> {
+        dbg!(&handle, &app_id, &parent_window, &title, &options);
+        dbg!(
+            FILE_CHOOSER_PROXY
+                .get()
+                .unwrap()
+                .save_file(&handle, app_id, parent_window, title, options)
+                .await
+        )
+    }
+
+    async fn save_files(
+        &self,
+        handle: ObjectPath<'_>,
+        app_id: &str,
+        parent_window: &str,
+        title: &str,
+        options: BTreeMap<&str, Value<'_>>,
+    ) -> zbus::fdo::Result<(u32, BTreeMap<String, OwnedValue>)> {
+        dbg!(&handle, &app_id, &parent_window, &title, &options);
+        dbg!(
+            FILE_CHOOSER_PROXY
+                .get()
+                .unwrap()
+                .save_files(&handle, app_id, parent_window, title, options)
+                .await
+        )
+    }
+}
diff --git a/tools/xdg-desktop-portal-spectrum-host/src/host_bus.rs b/tools/xdg-desktop-portal-spectrum-host/src/host_bus.rs
new file mode 100644
index 0000000..70444f4
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum-host/src/host_bus.rs
@@ -0,0 +1,100 @@
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, Mutex};
+
+use std::future::{poll_fn, Future};
+use std::task::{Poll, Waker};
+
+use zbus::{interface, Guid};
+
+struct BusImpl {
+    name_owned: bool,
+    waker: Option<Waker>,
+}
+
+pub struct Bus {
+    imp: Arc<Mutex<BusImpl>>,
+    name_owned_called: AtomicBool,
+}
+
+const DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER: u32 = 1;
+
+impl Bus {
+    pub fn new() -> Bus {
+        Self {
+            imp: Arc::new(Mutex::new(BusImpl {
+                name_owned: false,
+                waker: None,
+            })),
+            name_owned_called: AtomicBool::new(false),
+        }
+    }
+
+    pub fn name_owned(&self) -> impl Future<Output = ()> {
+        if self.name_owned_called.swap(true, Ordering::SeqCst) {
+            panic!("Bus::name_owned can only be called once");
+        }
+
+        let imp = Arc::clone(&self.imp);
+
+        poll_fn(move |context| {
+            let mut imp = imp.lock().unwrap();
+
+            if imp.name_owned {
+                Poll::Ready(())
+            } else {
+                imp.waker = Some(context.waker().clone());
+                Poll::Pending
+            }
+        })
+    }
+}
+
+#[interface(name = "org.freedesktop.DBus")]
+impl Bus {
+    async fn hello(&self) -> zbus::fdo::Result<String> {
+        eprintln!("Hello");
+        Ok(Guid::generate().to_string())
+    }
+
+    async fn add_match(&self, rule: &str) -> zbus::fdo::Result<()> {
+        eprintln!("AddMatch {:?}", rule);
+        Ok(())
+    }
+
+    async fn remove_match(&self, rule: &str) -> zbus::fdo::Result<()> {
+        eprintln!("RemoveMatch {:?}", rule);
+        Ok(())
+    }
+
+    async fn get_name_owner(&self, name: String) -> zbus::fdo::Result<String> {
+        eprintln!("GetNameOwner {:?}", name);
+        Err(zbus::fdo::Error::NameHasNoOwner(name))
+    }
+
+    async fn request_name(&mut self, name: &str, flags: u32) -> zbus::fdo::Result<u32> {
+        eprintln!("RequestName {:?} {:#x?}", name, flags);
+
+        if flags > 0x7 {
+            let message = format!("unrecognized flags {:#x?}", flags & !0x7);
+            return Err(zbus::fdo::Error::NotSupported(message));
+        }
+
+        if name != "org.freedesktop.impl.portal.desktop.gtk" {
+            let message = format!("refusing to assign name {:?}", name);
+            return Err(zbus::fdo::Error::AccessDenied(message));
+        }
+
+        let mut imp = self.imp.lock().unwrap();
+        imp.name_owned = true;
+        if let Some(waker) = imp.waker.take() {
+            waker.wake();
+        }
+
+        Ok(DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER)
+    }
+
+    async fn start_service_by_name(&self, name: String, flags: u32) -> zbus::fdo::Result<u32> {
+        eprintln!("StartServiceByName {:?} {:#x?}", name, flags);
+        Err(zbus::fdo::Error::ServiceUnknown(name))
+    }
+}
diff --git a/tools/xdg-desktop-portal-spectrum-host/src/main.rs b/tools/xdg-desktop-portal-spectrum-host/src/main.rs
new file mode 100644
index 0000000..c91a5d4
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum-host/src/main.rs
@@ -0,0 +1,272 @@
+mod guest_dbus;
+mod host_bus;
+
+use std::cmp::max;
+use std::env::args_os;
+use std::ffi::{CString, OsStr, OsString};
+use std::fs::File;
+use std::io::{self, ErrorKind};
+use std::os::linux::net::SocketAddrExt;
+use std::os::unix::net::{SocketAddr, UnixListener, UnixStream};
+use std::os::unix::prelude::*;
+use std::path::{Path, PathBuf};
+use std::ptr;
+use std::process::{exit, Command};
+use std::slice;
+use std::sync::OnceLock;
+
+use async_executor::Executor;
+use async_io::Async;
+use futures_lite::prelude::*;
+use futures_lite::stream::StreamExt;
+use libc::{mount, MS_BIND, MS_REC};
+use zbus::{ConnectionBuilder, Guid, MessageStream};
+
+use guest_dbus::{FileChooserImpl, FileChooserProxy};
+
+static VSOCK_UNIX_PATH: OnceLock<PathBuf> = OnceLock::new();
+static FILE_CHOOSER_PROXY: OnceLock<FileChooserProxy> = OnceLock::new();
+
+fn share_file(source_path: PathBuf, writable: bool) {
+    assert!(source_path.is_file());
+    assert!(writable);
+
+    let fs_root_dir = std::env::var_os("XDG_DESKTOP_PORTAL_SPECTRUM_HOST_FS_ROOT").unwrap();
+    let name = source_path.file_name().unwrap();
+
+    let dest_path = Path::new(&fs_root_dir).join(name);
+    
+    File::create(&dest_path).unwrap();
+
+    let c_source_path = CString::new(source_path.into_os_string().into_vec()).unwrap();
+    let c_dest_path = CString::new(dest_path.into_os_string().into_vec()).unwrap();
+
+    unsafe {
+        if mount(c_source_path.as_ptr(), c_dest_path.as_ptr(), ptr::null(), MS_BIND|MS_REC, ptr::null()) == -1 {
+            panic!("{}", io::Error::last_os_error());
+        }
+    }
+}
+
+async fn negotiate_version(conn: &mut Async<UnixStream>) -> Result<(), String> {
+    let mut num_versions = 0;
+    conn.read_exact(slice::from_mut(&mut num_versions))
+        .await
+        .map_err(|e| format!("reading number of versions supported by client: {e}"))?;
+
+    let mut client_versions = vec![0; num_versions.into()];
+    conn.read_exact(&mut client_versions)
+        .await
+        .map_err(|e| format!("reading versions supported by client: {e}"))?;
+
+    if !client_versions.contains(&1) {
+        let msg = format!(
+            "no supported protocol versions offered by client: {:?}",
+            client_versions
+        );
+        return Err(msg);
+    }
+
+    conn.write_all(&[1])
+        .await
+        .map_err(|e| format!("communicating chosen protocol version to client: {e}"))?;
+
+    Ok(())
+}
+
+async fn receive_port(conn: &mut Async<UnixStream>) -> Result<u32, String> {
+    negotiate_version(conn).await?;
+
+    let mut port = [0; 4];
+    conn.read_exact(&mut port)
+        .await
+        .map_err(|e| format!("reading port: {e}"))?;
+
+    if let Ok(1) = conn.read(&mut [0]).await {
+        return Err("unexpected data following port".to_string());
+    }
+
+    Ok(u32::from_be_bytes(port))
+}
+
+async fn connect_to_guest(port: u32) -> Result<Async<UnixStream>, String> {
+    let vsock_unix_path = VSOCK_UNIX_PATH.get().unwrap();
+    let mut vsock = Async::<UnixStream>::connect(vsock_unix_path)
+        .await
+        .map_err(|e| format!("connecting to {:?}: {e}", vsock_unix_path))?;
+    for part in [b"CONNECT ", port.to_string().as_bytes(), b"\n"] {
+        vsock
+            .write_all(part)
+            .await
+            .map_err(|e| format!("writing to VSOCK UNIX socket: {e}"))?;
+    }
+
+    // TODO: this shouldn't be an unwrap, because it could actually happen.
+    // TODO: MSG_PEEK?
+    let mut response = [0; 14]; // Length of "OK [u32::MAX]\n" is 14.
+    let min_response_len = "OK 0\n".len();
+    let mut response_len = 0usize;
+
+    while response_len.checked_sub(1).and_then(|i| response.get(i)) != Some(&b'\n')
+        && response_len < response.len()
+    {
+        let min_remaining = min_response_len.saturating_sub(response_len);
+        let end = max(min_remaining, response_len + 1);
+        let dest = response.get_mut(response_len..end).unwrap();
+        response_len += dest.len();
+        vsock
+            .read_exact(dest)
+            .await
+            .map_err(|e| format!("reading VSOCK connection response: {e}"))?;
+    }
+
+    if !response.starts_with(b"OK ") {
+        return Err(format!(
+            "unexpected response from Cloud Hypervisor VSOCK handshake: {:#x?}",
+            response
+        ));
+    }
+
+    Ok(vsock)
+}
+
+async fn run_guest_connection(mut conn: Async<UnixStream>) -> Result<(), String> {
+    let port = receive_port(&mut conn).await?;
+    let vsock = connect_to_guest(port).await?;
+
+    let guest_conn = ConnectionBuilder::socket(vsock)
+        .name("org.freedesktop.impl.portal.desktop.spectrum")
+        .unwrap()
+        .serve_at("/org/freedesktop/portal/desktop", FileChooserImpl)
+        .unwrap()
+        .build()
+        .await
+        .map_err(|e| format!("setting up connection to guest bus: {e}"))?;
+
+    eprintln!("Created org.freedesktop.impl.portal.desktop.spectrum.host on guest bus");
+
+    let mut guest_messages = MessageStream::from(guest_conn);
+    loop {
+        match guest_messages.try_next().await {
+            Ok(_) => (),
+            Err(zbus::Error::InputOutput(e)) if e.kind() == ErrorKind::BrokenPipe => break Ok(()),
+            Err(e) => return Err(format!("communicating with guest bus: {e}")),
+        }
+    }
+}
+
+fn read_argv() {
+    let mut args = args_os();
+    args.next();
+
+    if args.next().is_some() {
+        eprintln!("too many arguments");
+        exit(1);
+    }
+}
+
+async fn spawn_portal_impl() -> Result<(), Box<dyn std::error::Error>> {
+    let impl_conn_guid = Guid::generate();
+    let addr = SocketAddr::from_abstract_name(impl_conn_guid.as_bytes())?;
+    let impl_listener = Async::new(UnixListener::bind_addr(&addr)?)?;
+
+    let bus_addr = format!("unix:abstract={impl_conn_guid}");
+    Command::new("xdg-desktop-portal-gtk")
+        .env("DBUS_SESSION_BUS_ADDRESS", &bus_addr)
+        .spawn()?;
+
+    let (impl_conn, _) = impl_listener.accept().await?;
+
+    let host_bus = host_bus::Bus::new();
+    let name_owned = host_bus.name_owned();
+
+    let impl_conn = ConnectionBuilder::socket(impl_conn)
+        .p2p()
+        .server(Guid::generate())
+        .unwrap()
+        .serve_at("/org/freedesktop/DBus", host_bus)?
+        .build()
+        .await?;
+
+    name_owned.await;
+
+    FILE_CHOOSER_PROXY
+        .set(FileChooserProxy::new(&impl_conn, "org.freedesktop.impl.portal.desktop.gtk").await?)
+        .unwrap();
+
+    Ok(())
+}
+
+fn listening_vsock_path(connection: &UnixListener) -> io::Result<PathBuf> {
+    let Some(mut listening_addr) = connection
+        .local_addr()?
+        .as_pathname()
+        .map(Path::as_os_str)
+        .map(OsStr::to_os_string)
+        .map(OsString::into_vec)
+    else {
+        eprintln!("stdin is not listening on a path");
+        exit(1);
+    };
+
+    let mut i = listening_addr.len() - 1;
+
+    loop {
+        match (i != 0).then_some(listening_addr[i]) {
+            Some(b'0'..=b'9') => {
+                i -= 1;
+            }
+            Some(b'_') => {
+                break;
+            }
+            _ => {
+                let os_string = OsString::from_vec(listening_addr);
+                eprintln!("can't infer listening VSOCK path from {:?}", os_string);
+                exit(1);
+            }
+        }
+    }
+
+    listening_addr.truncate(i);
+    Ok(OsString::from_vec(listening_addr).into())
+}
+
+// TODO: let's not have main return a Result.
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    read_argv();
+
+    let ex = Executor::new();
+
+    async_io::block_on(ex.run(async {
+        // SAFETY: safe because we won't use fd 0 anywhere else.
+        let stdin = Async::try_from(unsafe { UnixListener::from_raw_fd(0) })?;
+
+        VSOCK_UNIX_PATH
+            .set(listening_vsock_path(stdin.get_ref())?)
+            .unwrap();
+
+        let mut portal_impl_spawned = false;
+
+        loop {
+            let (conn, _) = match stdin.accept().await {
+                Ok(conn) => conn,
+                Err(e) => {
+                    eprintln!("accepting connection from guest: {e}");
+                    continue;
+                }
+            };
+
+            if !portal_impl_spawned {
+                spawn_portal_impl().await?;
+                portal_impl_spawned = true;
+            }
+
+            ex.spawn(async move {
+                if let Err(e) = run_guest_connection(conn).await {
+                    eprintln!("guest connection error: {e}");
+                }
+            })
+            .detach();
+        }
+    }))
+}
diff --git a/tools/xdg-desktop-portal-spectrum/default.nix b/tools/xdg-desktop-portal-spectrum/default.nix
new file mode 100644
index 0000000..41d34bc
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum/default.nix
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is>
+
+import ../../lib/call-package.nix (
+{ src, lib, stdenv, meson, ninja, pkg-config, dbus }:
+
+stdenv.mkDerivation (finalAttrs: {
+  name = "xdg-desktop-portal-spectrum";
+
+  src = lib.fileset.toSource {
+    root = ../..;
+    fileset = lib.fileset.intersection src ./.;
+  };
+  sourceRoot = "source/tools/xdg-desktop-portal-spectrum";
+
+  nativeBuildInputs = [ meson ninja pkg-config ];
+  buildInputs = [ dbus ];
+
+  # TODO: clang-tidy
+})
+) (_: {})
diff --git a/tools/xdg-desktop-portal-spectrum/meson.build b/tools/xdg-desktop-portal-spectrum/meson.build
new file mode 100644
index 0000000..9303629
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum/meson.build
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2024 Alyssa Ross <hi@alyssa.is>
+
+project('xdg-desktop-portal-spectrum', 'c',
+  default_options : [ 'warning_level=3' ])
+
+add_project_arguments('-D_GNU_SOURCE', language : 'c')
+
+dbus = dependency('dbus-1')
+
+executable('xdg-desktop-portal-spectrum', 'xdg-desktop-portal-spectrum.c',
+  dependencies : dbus,
+  install : true)
+
+install_data('spectrum.portal',
+  install_dir : get_option('datadir') / 'xdg-desktop-portal/portals')
+
+conf_data = configuration_data()
+conf_data.set('bindir', get_option('prefix') / get_option('bindir'))
+configure_file(
+  input : 'org.freedesktop.impl.portal.desktop.spectrum.service.in',
+  output : 'org.freedesktop.impl.portal.desktop.spectrum.service',
+  configuration : conf_data,
+  install_dir : get_option('datadir') / 'dbus-1/services')
diff --git a/tools/xdg-desktop-portal-spectrum/org.freedesktop.impl.portal.desktop.spectrum.service.in b/tools/xdg-desktop-portal-spectrum/org.freedesktop.impl.portal.desktop.spectrum.service.in
new file mode 100644
index 0000000..9c4a2cd
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum/org.freedesktop.impl.portal.desktop.spectrum.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.freedesktop.impl.portal.desktop.spectrum
+Exec=@bindir@/xdg-desktop-portal-spectrum
diff --git a/tools/xdg-desktop-portal-spectrum/spectrum.portal b/tools/xdg-desktop-portal-spectrum/spectrum.portal
new file mode 100644
index 0000000..08e3eb7
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum/spectrum.portal
@@ -0,0 +1,3 @@
+[portal]
+DBusName=org.freedesktop.impl.portal.desktop.spectrum
+Interfaces=org.freedesktop.impl.portal.FileChooser
diff --git a/tools/xdg-desktop-portal-spectrum/xdg-desktop-portal-spectrum.c b/tools/xdg-desktop-portal-spectrum/xdg-desktop-portal-spectrum.c
new file mode 100644
index 0000000..24c40b3
--- /dev/null
+++ b/tools/xdg-desktop-portal-spectrum/xdg-desktop-portal-spectrum.c
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: EUPL-1.2+
+// SPDX-FileCopyrightText: 2024 Alyssa Ross <hi@alyssa.is>
+
+#include <arpa/inet.h>
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <linux/vm_sockets.h>
+
+#include <dbus/dbus.h>
+
+static const uint32_t HOST_PORT = 219;
+
+static int parse_u32(const char *s, uint32_t *v)
+{
+	char *end;
+
+	errno = EINVAL;
+	if (!s || !isdigit(s[0]))
+		return -1;
+
+	errno = 0;
+	*v = strtol(s, &end, 10);
+	if (errno)
+		return -1;
+	if (*end) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	return 0;
+}
+
+static int write_all(int fd, const void *buf, size_t len)
+{
+	int r;
+
+	do {
+		r = write(fd, buf, len);
+		if (r == -1)
+			return -1;
+		buf = (char *)buf + r;
+		len -= r;
+	} while (len);
+
+	return 0;
+}
+
+static void send_port(uint32_t port)
+{
+	struct sockaddr_vm addr = {
+		.svm_family = AF_VSOCK,
+		.svm_cid = VMADDR_CID_HOST,
+		.svm_port = HOST_PORT,
+	};
+	char handshake[] = { 1, 1 };
+	char version;
+	int sock = socket(AF_VSOCK, SOCK_STREAM, 0);
+
+	port = htonl(port);
+
+	if (sock == -1)
+		err(EXIT_FAILURE, "creating vsock socket");
+	
+	if (connect(sock, (struct sockaddr *)&addr, sizeof addr) == -1)
+		err(EXIT_FAILURE, "connecting to cid %" PRIu32 " port %" PRIu32,
+		    addr.svm_cid, addr.svm_port);
+	
+	if (write_all(sock, handshake, sizeof handshake) == -1)
+		err(EXIT_FAILURE, "writing handshake to vsock socket");
+
+	if (read(sock, &version, 1) == -1)
+		err(EXIT_FAILURE, "reading handshake version");
+	if (version != 1)
+		err(EXIT_FAILURE, "unexpected protocol version %d", version);
+
+	if (write_all(sock, &port, sizeof port) == -1)
+		err(EXIT_FAILURE, "writing port to vsock socket");
+}
+
+int main(void)
+{
+	char *addr = getenv("DBUS_STARTER_ADDRESS");
+
+	DBusAddressEntry **entries;
+	int entries_len, i;
+	DBusError error;
+
+	const char *port_str;
+	uint32_t port;
+
+	if (!addr)
+		errx(EXIT_FAILURE, "DBUS_STARTER_ADDRESS not set");
+
+	warnx("DBUS_STARTER_ADDRESS: '%s'", addr);
+
+	if (!dbus_parse_address(addr, &entries, &entries_len, &error))
+		errx(EXIT_FAILURE, "parsing D-Bus address '%s': %s",
+		     addr, error.message);
+
+	for (i = 0; i < entries_len; i++) {
+		if (strcmp(dbus_address_entry_get_method(entries[i]), "vsock"))
+			continue;
+
+		if (!(port_str = dbus_address_entry_get_value(entries[i], "port")))
+			errx(EXIT_FAILURE, "missing vsock port in D-Bus address '%s'",
+			     addr);
+
+		if (parse_u32(port_str, &port) == -1)
+			err(EXIT_FAILURE, "D-Bus address vsock port");
+
+		send_port(port);
+		return 0;
+	}
+}