vmsilo/docs/superpowers/plans/2026-03-23-remove-python-sommelier.md
2026-03-24 11:56:43 +00:00

23 KiB

Remove Python & Sommelier, Add Rust Tools

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Remove Python runtime dependency and sommelier wayland proxy; replace with two new Rust CLI tools in the vmsilo-tools workspace.

Architecture: Two new Rust crates (desktop-file and vsock-proxy) are added to the existing vmsilo-tools/ Cargo workspace. Nix modules are updated to call the new binaries instead of Python scripts. Sommelier is removed entirely, simplifying wayland proxy code to a single codepath.

Tech Stack: Rust (edition 2021), Nix/NixOS modules, shell scripts

Important context:

  • nix build uses the git index — all new files must be git add'd before building.
  • The vmsilo-tools workspace builds as a single package: cfg._internal."vmsilo-tools". Binaries are at ${cfg._internal."vmsilo-tools"}/bin/<name>.
  • There are no automated tests for the Nix modules. Rust crates should have unit tests.
  • Run nix fmt before committing.
  • Don't vary guest rootfs contents per-VM; gate features on the host side only.

Task 1: Add desktop-file crate to vmsilo-tools workspace

Files:

  • Create: vmsilo-tools/desktop-file/Cargo.toml
  • Create: vmsilo-tools/desktop-file/src/main.rs
  • Create: vmsilo-tools/desktop-file/src/parser.rs
  • Modify: vmsilo-tools/Cargo.toml (add workspace member)

This is a generic .desktop file manipulation tool. The freedesktop .desktop format is INI-like: [Group] headers, Key=Value lines, comments (#), blank lines. Locale keys like Name[fr] are distinct keys.

Subcommands:

  • get-key <file> <group> <key> — print value, exit 0 if found, exit 1 if missing
  • set-key <file> <group> <key> <value> — output full file with key set (create key/group if missing)
  • list-groups <file> — one group name per line
  • list-entries <file> <group> — one key per line for group
  • filter-groups <file> <group> [<group>...] — output file keeping only listed groups
  • filter-keys <file> <group> <key> [<key>...] — output file keeping only listed keys in group; other groups pass through unchanged

All commands accept - as file argument to read from stdin. Output to stdout.

  • Step 1: Create desktop-file/Cargo.toml
[package]
name = "vmsilo-desktop-file"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1"
  • Step 2: Add workspace member

In vmsilo-tools/Cargo.toml, change members = ["tap-open"] to members = ["tap-open", "desktop-file"].

  • Step 3: Write the parser module (src/parser.rs)

Line-oriented parser that classifies each line as: blank/comment, group header [Name], or key-value Key=Value. Preserves original text for passthrough. Data model:

/// A single line from a .desktop file, classified but preserving original text.
pub enum Line {
    /// Blank line or comment
    Other(String),
    /// Group header: [GroupName]
    Group { name: String, raw: String },
    /// Key=Value entry
    Entry { key: String, value: String, raw: String },
}

/// Parsed .desktop file preserving order and formatting.
pub struct DesktopFile {
    pub lines: Vec<Line>,
}

Implement:

  • DesktopFile::parse(input: &str) -> DesktopFile — parse lines
  • DesktopFile::get_key(group: &str, key: &str) -> Option<&str> — find key in group
  • DesktopFile::set_key(group: &str, key: &str, value: &str) — set key (create if needed, create group if needed)
  • DesktopFile::list_groups() -> Vec<&str> — unique group names in order
  • DesktopFile::list_entries(group: &str) -> Vec<&str> — key names in group
  • DesktopFile::filter_groups(keep: &[&str]) — remove groups not in keep list
  • DesktopFile::filter_keys(group: &str, keep: &[&str]) — remove keys not in keep list for given group, other groups unchanged
  • DesktopFile::write(&self) -> String — reconstruct file text

Key parsing rules:

  • Group header: line starts with [, ends with ] (after trim)

  • Key-value: split on first =

  • Everything else is Other (comments, blank lines)

  • Preserve original line text in raw field for exact passthrough when not modified

  • Step 4: Write parser unit tests

Test at minimum:

  • Parse a file with multiple groups, keys, comments, blank lines

  • get_key returns correct value, returns None for missing key/group

  • set_key on existing key changes value

  • set_key on new key in existing group appends it

  • set_key on new key in new group creates group at end

  • list_groups returns groups in order

  • list_entries returns keys in order

  • filter_groups keeps only listed groups plus their keys

  • filter_keys keeps only listed keys in target group, other groups unchanged

  • Round-trip: parse then write preserves original text

  • Locale keys (Name[fr]) treated as distinct from Name

  • Step 5: Run tests

Run: cd /home/david/git/vmsilo/vmsilo-tools && cargo test -p vmsilo-desktop-file Expected: All tests pass.

  • Step 6: Write src/main.rs

Parse CLI args manually (no clap dependency — keep it minimal like tap-open). Read file (or stdin if -), dispatch to parser methods, write output to stdout.

Usage:
  desktop-file get-key <file> <group> <key>
  desktop-file set-key <file> <group> <key> <value>
  desktop-file list-groups <file>
  desktop-file list-entries <file> <group>
  desktop-file filter-groups <file> <group> [<group>...]
  desktop-file filter-keys <file> <group> <key> [<key>...]

Exit codes: 0 = success, 1 = key/group not found (get-key), 2 = usage error.

  • Step 7: Write main integration tests

Test stdin piping: write a test that calls the binary as a subprocess, pipes input via stdin with - arg, verifies stdout output. Test the pipeline pattern: set-key | set-key | filter-keys.

  • Step 8: Run all tests

Run: cd /home/david/git/vmsilo/vmsilo-tools && cargo test -p vmsilo-desktop-file Expected: All tests pass.

  • Step 9: Commit
cd /home/david/git/vmsilo
git add vmsilo-tools/desktop-file/ vmsilo-tools/Cargo.toml
# Cargo.lock will be updated — add it too
git add vmsilo-tools/Cargo.lock
git commit -m "Add desktop-file tool to vmsilo-tools workspace"

Task 2: Add vsock-proxy crate to vmsilo-tools workspace

Files:

  • Create: vmsilo-tools/vsock-proxy/Cargo.toml
  • Create: vmsilo-tools/vsock-proxy/src/main.rs
  • Modify: vmsilo-tools/Cargo.toml (add workspace member)

Synchronous vsock CONNECT handshake + stdin/stdout proxy for cloud-hypervisor hybrid vsock.

Usage: vmsilo-vsock-proxy <socket-path> <port>

Protocol:

  1. Connect to Unix socket at <socket-path>
  2. Send CONNECT <port>\n
  3. Read response byte-by-byte until \n
  4. Validate response starts with OK
  5. Write any data received after the OK line to stdout
  6. Bidirectional proxy: stdin->socket (spawned thread), socket->stdout (main thread)
  7. On stdin EOF: shutdown(Write) on socket (half-close)
  8. Continue reading socket->stdout until EOF
  9. Exit 0 on success, non-zero on handshake failure or connection error

Reference implementation: The existing Rust CONNECT handshake in vmsilo-dbus-proxy/src/host/vsock.rs:20-60 does the same protocol (async/tokio). This tool is the synchronous, standalone equivalent.

  • Step 1: Create vsock-proxy/Cargo.toml
[package]
name = "vmsilo-vsock-proxy"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1"
  • Step 2: Add workspace member

In vmsilo-tools/Cargo.toml, add "vsock-proxy" to members list (should now be ["tap-open", "desktop-file", "vsock-proxy"]).

  • Step 3: Write src/main.rs

Implementation outline:

fn main() -> Result<()> {
    let args: Vec<String> = env::args().collect();
    // Validate: exactly 3 args (binary, socket_path, port)

    let socket_path = &args[1];
    let port: u32 = args[2].parse()?;

    // Connect to Unix socket
    let stream = UnixStream::connect(socket_path)?;

    // Send CONNECT handshake
    // write_all(format!("CONNECT {port}\n").as_bytes())

    // Read response byte-by-byte until \n
    // Validate starts with "OK "
    // If any trailing data after \n, write to stdout

    // Clone stream for the stdin->socket thread
    let writer = stream.try_clone()?;

    // Spawn stdin->socket thread
    // On stdin EOF: writer.shutdown(Shutdown::Write)

    // Main thread: socket->stdout loop
    // Read until EOF, write to stdout

    // Wait for stdin thread (optional, just exit)
    Ok(())
}

Use std::os::unix::net::UnixStream, std::net::Shutdown, std::thread, std::io::{Read, Write, stdin, stdout}. No async.

  • Step 4: Write unit tests for handshake

Use std::os::unix::net::UnixListener to create a mock server in a tempdir. Test:

  • Successful handshake (send CONNECT, get OK, verify stream works)

  • Failed handshake (server responds with error)

  • Trailing data after OK line is forwarded

  • Step 5: Run tests

Run: cd /home/david/git/vmsilo/vmsilo-tools && cargo test -p vmsilo-vsock-proxy Expected: All tests pass.

  • Step 6: Commit
cd /home/david/git/vmsilo
git add vmsilo-tools/vsock-proxy/ vmsilo-tools/Cargo.toml vmsilo-tools/Cargo.lock
git commit -m "Add vsock-proxy tool to vmsilo-tools workspace"

Task 3: Rewrite desktop.nix to use desktop-file tool, remove Python

Files:

  • Modify: modules/desktop.nix:97-175 (remove processDesktopScript, pythonWithPyxdg; rewrite shell function)

The shell function process_desktop in mkDesktopEntries (line 224) currently calls python3 processDesktopScript. Replace with calls to vmsilo-desktop-file.

  • Step 1: Add desktopFile variable

At the top of the let block in desktop.nix (after cfg = config.programs.vmsilo;), add:

desktopFile = "${cfg._internal."vmsilo-tools"}/bin/vmsilo-desktop-file";
  • Step 2: Delete the Python script and interpreter

Remove lines 96-175 from desktop.nix:

  • The processDesktopScript definition (lines 96-172)

  • The pythonWithPyxdg definition (lines 174-175)

  • Step 3: Rewrite the process_desktop shell function

Replace the body of process_desktop (currently at lines 224-238) with:

process_desktop() {
  local desktop="$1"
  local pkg="$2"

  # Get output filename
  local basename
  basename=$(basename "$desktop")
  local outfile="$out/share/applications/vmsilo.${vm.name}.$basename"

  # Check filters — skip NoDisplay=true and non-Application types
  local nodisplay dtype
  nodisplay=$(${desktopFile} get-key "$desktop" "Desktop Entry" NoDisplay) || nodisplay=""
  if [ "$nodisplay" = "true" ]; then return; fi
  dtype=$(${desktopFile} get-key "$desktop" "Desktop Entry" Type) || dtype=""
  if [ -n "$dtype" ] && [ "$dtype" != "Application" ]; then return; fi

  # Get original values for transformation
  local icon name exec_val
  icon=$(${desktopFile} get-key "$desktop" "Desktop Entry" Icon) || icon=""
  name=$(${desktopFile} get-key "$desktop" "Desktop Entry" Name) || name=""
  exec_val=$(${desktopFile} get-key "$desktop" "Desktop Entry" Exec) || exec_val=""

  # Build transformation pipeline
  {
    if [ -n "$name" ]; then
      ${desktopFile} set-key "$desktop" "Desktop Entry" Name "${vm.name}: $name"
    else
      cat "$desktop"
    fi
  } \
  | ${desktopFile} set-key - "Desktop Entry" Exec "vm-run ${vm.name} $exec_val" \
  | ${desktopFile} set-key - "Desktop Entry" Icon "vmsilo.${vm.name}.''${icon:-unknown}" \
  | ${desktopFile} set-key - "Desktop Entry" Categories "X-Vmsilo-${vm.name};" \
  | ${desktopFile} set-key - "Desktop Entry" X-VmSilo-Color "${vm.color}" \
  | ${desktopFile} filter-keys - "Desktop Entry" Type Version Name GenericName Comment Icon Exec Terminal Categories Keywords NoDisplay OnlyShowIn NotShowIn X-VmSilo-Color \
  | ${desktopFile} filter-groups - "Desktop Entry" \
  > "$outfile"

  # ... icon copying logic continues unchanged below ...

The rest of the function (icon copying, lines 240-277) stays as-is — it uses $icon which is still set.

  • Step 4: Verify no Python references remain in desktop.nix

Search for python, pyxdg, processDesktop in the file. Should be zero matches.

  • Step 5: Run nix fmt

Run: cd /home/david/git/vmsilo && nix fmt

  • Step 6: Commit
git add modules/desktop.nix
git commit -m "Replace Python processDesktopScript with desktop-file tool"

Task 4: Rewrite scripts.nix to use vsock-proxy, remove Python

Files:

  • Modify: modules/scripts.nix:229-312 (rewrite mkChVsockConnectScript, remove pyProxy)

  • Modify: modules/scripts.nix:375-378 (update stale comment about Python)

  • Step 1: Rewrite mkChVsockConnectScript

Replace the entire function (lines 229-312) with:

mkChVsockConnectScript =
  vmName: port:
  let
    vsockProxy = "${cfg._internal."vmsilo-tools"}/bin/vmsilo-vsock-proxy";
  in
  pkgs.writeShellScript "vsock-connect-${vmName}-${toString port}" ''
    VSOCK_SOCKET="/run/vmsilo/${vmName}/vsock.socket"
    PORT=${toString port}
    TIMEOUT=30
    ELAPSED=0

    # Wait for vsock socket to appear
    while [ $ELAPSED -lt $TIMEOUT ] && [ ! -S "$VSOCK_SOCKET" ]; do
      sleep 0.5
      ELAPSED=$((ELAPSED + 1))
    done
    [ -S "$VSOCK_SOCKET" ] || { echo "Timeout: vsock socket not found" >&2; exit 1; }

    # Retry until vsock port is ready (guest command listener may not be up yet).
    ELAPSED=0
    while [ $ELAPSED -lt $TIMEOUT ]; do
      ${vsockProxy} "$VSOCK_SOCKET" "$PORT" && exit 0
      sleep 0.5
      ELAPSED=$((ELAPSED + 1))
    done

    echo "Timeout waiting for VM ${vmName} vsock:${toString port}" >&2
    exit 1
  '';

This removes pyProxy and the pkgs.python3 dependency entirely.

  • Step 2: Update the stale comment in vmRunScript

At line 376-377, change:

# -t5: wait up to 5s for response after stdin EOF (default 0.5s is too short
#   for cloud-hypervisor proxy startup: Python interpreter + CONNECT handshake)

to:

# -t5: wait up to 5s for response after stdin EOF (default 0.5s is too short
#   for cloud-hypervisor proxy startup and CONNECT handshake)
  • Step 3: Verify no Python references remain in scripts.nix

Search for python, pyProxy in the file. Should be zero matches.

  • Step 4: Run nix fmt

Run: cd /home/david/git/vmsilo && nix fmt

  • Step 5: Commit
git add modules/scripts.nix
git commit -m "Replace Python vsock-proxy with vmsilo-vsock-proxy tool"

Task 5: Replace socat CONNECT probe with vsock-proxy in USB service

Files:

  • Modify: modules/services.nix:668-674 (replace socat probe with vsock-proxy)

  • Step 1: Replace the cloud-hypervisor CONNECT probe

In modules/services.nix, the USB service start script (inside mkUsbServices) has a block at lines 668-674 that checks if the guest vsock port 5002 is reachable. The cloud-hypervisor branch currently uses socat:

if echo "CONNECT 5002" | socat - UNIX-CONNECT:/run/vmsilo/${vm.name}/vsock.socket 2>/dev/null | head -1 | grep -q '^OK '; then break; fi

Replace the entire ${if vm.hypervisor == "crosvm" then ... else ...} block (lines 669-674) with:

${
  if vm.hypervisor == "crosvm" then
    "if ${pkgs.socat}/bin/socat -u OPEN:/dev/null VSOCK-CONNECT:${toString vm.id}:5002 2>/dev/null; then break; fi"
  else
    "if ${cfg._internal."vmsilo-tools"}/bin/vmsilo-vsock-proxy /run/vmsilo/${vm.name}/vsock.socket 5002 </dev/null 2>/dev/null; then break; fi"
}

The crosvm path stays as socat (kernel vsock, no CONNECT protocol). The cloud-hypervisor path uses vmsilo-vsock-proxy with stdin from /dev/null — handshake succeeds, stdin EOF triggers shutdown(Write), socket closes, exit 0.

  • Step 2: Update the stale comment

At approximately line 666 in modules/services.nix, update the comment:

# Use socat probe for crosvm (kernel vsock), socat via unix socket for cloud-hypervisor

to:

# Use socat probe for crosvm (kernel vsock), vsock-proxy for cloud-hypervisor
  • Step 3: Run nix fmt

Run: cd /home/david/git/vmsilo && nix fmt

  • Step 4: Commit
git add modules/services.nix
git commit -m "Use vmsilo-vsock-proxy for CH vsock probe in USB service"

Task 6: Remove sommelier entirely

Files:

  • Delete: packages/sommelier.nix
  • Modify: flake.nix:170 (remove sommelier package build)
  • Modify: flake.nix:235 (remove sommelier injection)
  • Modify: modules/options.nix:230-239 (remove waylandProxy.type option)
  • Modify: modules/options.nix:250 (update logLevel description)
  • Modify: modules/options.nix:1018-1022 (remove internal sommelier option)
  • Modify: modules/lib/vm-config.nix:94,98 (remove sommelier and waylandProxy args)
  • Modify: rootfs-nixos/default.nix:12,15,39,41-42 (remove sommelier/waylandProxy params, update comment)
  • Modify: rootfs-nixos/guest/wayland.nix (remove sommelier codepath, simplify)

Important: All sommelier removal must happen in a single commit to avoid broken intermediate states (options.nix declares the option, vm-config.nix references it — removing one without the other breaks evaluation).

  • Step 1: Delete packages/sommelier.nix
rm packages/sommelier.nix
  • Step 2: Remove sommelier from flake.nix

Remove line 170:

        sommelier = nixpkgs.legacyPackages.${system}.callPackage ./packages/sommelier.nix { };

Remove line 235 (the sommelier = line in the _internal block):

              sommelier = pkgs.callPackage ./packages/sommelier.nix { };
  • Step 3: Remove waylandProxy.type option from options.nix

Remove the entire waylandProxy.type option block at lines 231-239 (the type = lib.mkOption { type = lib.types.enum [...]; ... } block, but keep the waylandProxy = { wrapper and logLevel option).

The structure should remain:

waylandProxy = {
  logLevel = lib.mkOption { ... };
};
  • Step 4: Update waylandProxy.logLevel description

At line 250, change:

description = "Log level for wayland-proxy-virtwl. Ignored for sommelier.";

to:

description = "Log level for wayland-proxy-virtwl.";
  • Step 5: Remove internal sommelier option

Remove lines 1018-1022:

      sommelier = lib.mkOption {
        type = lib.types.package;
        description = "sommelier package (injected by flake).";
        internal = true;
      };
  • Step 6: Update modules/lib/vm-config.nix

At line 94, change:

inherit (cfg._internal) wayland-proxy-virtwl sommelier;

to:

inherit (cfg._internal) wayland-proxy-virtwl;

Remove line 98 (waylandProxy = vm.waylandProxy.type;). Keep line 99 (waylandProxyLogLevel = vm.waylandProxy.logLevel;).

  • Step 7: Update rootfs-nixos/default.nix

Remove sommelier, from the function args (line 12).

Remove waylandProxy ? "wayland-proxy-virtwl", from the function args (line 15). Keep waylandProxyLogLevel ? "info", (line 16).

Update the comment at line 39 from:

# Pass wayland proxy packages and selection to the configuration

to:

# Pass wayland proxy package and config to the configuration

Remove these _module.args injections (lines 41-42):

_module.args.sommelier = sommelier;
_module.args.waylandProxy = waylandProxy;

Keep _module.args.waylandProxyLogLevel = waylandProxyLogLevel; (line 43).

  • Step 8: Simplify rootfs-nixos/guest/wayland.nix

Update the file header comment (line 3) from:

# - wayland-proxy-virtwl or sommelier (mutually exclusive)

to:

# - wayland-proxy-virtwl proxy service

Remove sommelier, and waylandProxy, from the function args (lines 9-10). Keep waylandProxyLogLevel,.

Remove the sommelier-related let variables (lines 17-19 — NOT line 16 which is kernelParamHelper):

  isSommelier = waylandProxy == "sommelier";
  proxyServiceName = if isSommelier then "sommelier" else "wayland-proxy-virtwl";
  waylandDisplayName = if isSommelier then "wayland-0" else "wayland-1";

On the wayland-proxy-virtwl service (line 23), remove the lib.mkIf (!isSommelier) guard — the service is now unconditional.

Delete the entire sommelier service block (lines 63-94).

In vmsilo-session-setup (line 110), change:

requires = [ "${proxyServiceName}.service" ];
after = [ "${proxyServiceName}.service" ];

to:

requires = [ "wayland-proxy-virtwl.service" ];
after = [ "wayland-proxy-virtwl.service" ];

In the session setup script (line 128), change:

export WAYLAND_DISPLAY="${waylandDisplayName}"

to:

export WAYLAND_DISPLAY="wayland-1"
  • Step 9: Run nix fmt

Run: cd /home/david/git/vmsilo && nix fmt

  • Step 10: Commit
git add -u packages/sommelier.nix flake.nix modules/options.nix modules/lib/vm-config.nix rootfs-nixos/default.nix rootfs-nixos/guest/wayland.nix
git commit -m "Remove sommelier wayland proxy entirely"

Task 7: Update README.md and CLAUDE.md

Files:

  • Modify: README.md:383-389 (simplify wayland proxy section)

  • Modify: README.md:590 (remove sommelier reference)

  • Modify: CLAUDE.md (update if needed)

  • Step 1: Simplify the Wayland Proxy section in README.md

Replace lines 383-389:

### Wayland Proxy

```nix
waylandProxy.type = "wayland-proxy-virtwl";  # Default: wayland-proxy-virtwl by Thomas Leonard
waylandProxy.type = "sommelier";             # ChromeOS sommelier (experiment, does not work currently)
waylandProxy.logLevel = "debug";             # Log level for wayland-proxy-virtwl (default: info)

with:
```markdown
### Wayland Proxy

```nix
waylandProxy.logLevel = "debug";  # Log level for wayland-proxy-virtwl (default: info)

- [ ] **Step 2: Update architecture section in README.md**

At line 590, change:
  • Wayland proxy for GPU passthrough (wayland-proxy-virtwl or sommelier)
to:
  • Wayland proxy for GPU passthrough (wayland-proxy-virtwl)

- [ ] **Step 3: Update CLAUDE.md if needed**

Check for any references to Python, sommelier, or `waylandProxy.type` in CLAUDE.md. Update:
- If `waylandProxy` option documentation mentions the type enum, remove it
- If sommelier is mentioned anywhere, remove it
- No Python-specific references are expected but verify

- [ ] **Step 4: Run `nix fmt`**

Run: `cd /home/david/git/vmsilo && nix fmt`

- [ ] **Step 5: Commit**

```bash
git add README.md CLAUDE.md
git commit -m "Update docs: remove sommelier references, simplify wayland proxy section"

Task 8: Build verification

Files: None (verification only)

  • Step 1: Verify Rust tools build
cd /home/david/git/vmsilo/vmsilo-tools && cargo build && cargo test

Expected: All crates build, all tests pass.

  • Step 2: Verify no Python references remain

Search the entire modules/ and rootfs-nixos/ directories for python, pyxdg, sommelier. Only valid hits should be in git history, not in current files.

cd /home/david/git/vmsilo
grep -r "python\|pyxdg\|sommelier" modules/ rootfs-nixos/ packages/ --include="*.nix" -l

Expected: No matches (or only false positives like comments mentioning removal).

  • Step 3: Verify nix fmt is clean
cd /home/david/git/vmsilo && nix fmt -- --check .

Expected: No formatting changes needed.

  • Step 4: Verify nix build succeeds
cd /home/david/git/vmsilo && git add -A && nix build .#

Expected: Build succeeds. Note: git add -A is needed because nix build uses git index for source filtering.