Skip to content

Sanitized MCP-host environment strips DBUS_SESSION_BUS_ADDRESS; guard refuses a working desktop #7

Description

@timrichardson

What happened

Hermes Agent (and other MCP hosts) spawn stdio MCP servers with a filtered environment to avoid leaking credentials: only a safe baseline passes (PATH, HOME, USER, LANG, LC_ALL, TERM, SHELL, TMPDIR, XDG_*). DBUS_SESSION_BUS_ADDRESS is stripped.

The guard in deskwright/shell.py (_gdbus) refuses before attempting to connect when the env var is unset:

if not os.environ.get("DBUS_SESSION_BUS_ADDRESS"):
    raise ToolError("DBUS_SESSION_BUS_ADDRESS is not set, so the session bus is unreachable. ...")

Result: every extension/window tool call fails with extension_unavailable on the user's own, perfectly working desktop.

Why the guard over-triggers

The env var is an address hint, not the mechanism. Since the systemd user bus, GLib (and gdbus, and AT-SPI) connect to $XDG_RUNTIME_DIR/bus when the variable is absent — and XDG_RUNTIME_DIR survives every sanitized baseline.

Scripted reproducer

No MCP client, no dependencies. Run from a deskwright checkout, in the user's graphical session:

#!/usr/bin/env python3
"""Reproducer: the session-bus guard refuses a desktop it can actually reach."""
import os
import subprocess
import sys

BASELINE = ("PATH", "HOME", "USER", "LANG", "LC_ALL", "TERM", "SHELL", "TMPDIR",
            "XDG_RUNTIME_DIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME",
            "XDG_STATE_HOME", "XDG_CACHE_HOME")
sanitized = {k: os.environ[k] for k in BASELINE if k in os.environ}
sanitized.pop("DBUS_SESSION_BUS_ADDRESS", None)  # what the host strips

print(f"[1] sanitized env ({len(sanitized)} vars), "
      "DBUS_SESSION_BUS_ADDRESS absent:",
      "DBUS_SESSION_BUS_ADDRESS" not in sanitized)

probe = subprocess.run(
    ["gdbus", "introspect", "--session", "--dest", "org.freedesktop.DBus",
     "--object-path", "/org/freedesktop/DBus"],
    env=sanitized, capture_output=True, text=True, timeout=15)
if probe.returncode != 0:
    sys.exit("Prerequisite not met: no session bus reachable even via the "
             f"XDG_RUNTIME_DIR fallback on this machine:\n{probe.stderr}\n"
             "(This reproducer needs the user's graphical session.)")
print("[2] gdbus reaches the session bus in that env: yes")

code = (
    "from deskwright.shell import _gdbus\n"
    "try:\n"
    "    _gdbus('Ping')\n"
    "    print('[3] OK: the guard let the call through to the extension')\n"
    "except Exception as e:\n"
    "    print(f'[3] REPRODUCED: guard refused a reachable desktop: {e}')\n"
)
r = subprocess.run([sys.executable, "-c", code], env=sanitized,
                   capture_output=True, text=True, timeout=30)
print(r.stdout.strip() or r.stderr.strip())

Observed on Ubuntu 26.04, GNOME Wayland:

  • origin/main: step 2 succeeds, step 3 prints REPRODUCED: guard refused a reachable desktop: DBUS_SESSION_BUS_ADDRESS is not set... — the contradiction is the bug.
  • fix branch (timrichardson:fix/session-bus-fallback): [3] OK: the guard let the call through to the extension.

Step 2 succeeding while step 3 fails is the whole argument: same environment, same machine, gdbus reaches the bus the guard says is unreachable.

On a machine with no graphical session the script exits early with Prerequisite not met, so it can't mislead a reviewer on a headless box.

Second instance of the same pattern

execution.session_key() hashes the raw env var ("unbound" fallback) for desktop-identity/lease keys. A fallback-connected server and an explicit-connected server on one desktop get different keys.

Fix direction

Resolve the reachable bus (explicit address → $XDG_RUNTIME_DIR/bus socket) and refuse only when neither exists — the bare ssh login / system service case the diagnostic exists for. PR incoming on top of this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions