mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:36 +01:00
myNurPkgs evaluated a second nixpkgs just for four packages. Apply the NUR repo's own overlay instead, so they come from the host's pkgs. Darwin's pkgs now gets nixpkgsConfig too: it set nixpkgs.pkgs, which ignores nixpkgs.config, so only myNurPkgs carried the npm mirror there. Assisted-by: Claude Code (claude-opus-5-5)
230 lines
7.8 KiB
Nix
230 lines
7.8 KiB
Nix
{
|
|
pkgs,
|
|
config,
|
|
lib,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.modules.desktop.computerUse;
|
|
|
|
display = ":0";
|
|
screen = "1920x1080x24";
|
|
|
|
# Keep this list minimal: the goal is to look like a real user device, not to
|
|
# harden the browser. The browser goes out through Clash Verge's proxy, so
|
|
# non-proxied WebRTC UDP must stay disabled (a real IP leak would contradict
|
|
# the exit region); everything else is a stock-looking setting.
|
|
#
|
|
# Deliberately absent: every automation/CDP switch. No
|
|
# `--remote-debugging-port` (a persistent, page-detectable endpoint) and no
|
|
# `--enable-automation` / `--disable-blink-features=AutomationControlled`.
|
|
# Nothing here drives the browser over CDP, so `navigator.webdriver` is
|
|
# already false without a flag that only marks the setup as automated.
|
|
browserFlags = [
|
|
"--force-webrtc-ip-handling-policy=disable_non_proxied_udp"
|
|
# Required for the AT-SPI accessibility tree the driver reads; the driver
|
|
# only walks the tree, it cannot make Chromium build one. This is the one
|
|
# flag with a realism cost (few real users force a11y on) and stays only
|
|
# because dropping it risks an empty tree.
|
|
"--force-renderer-accessibility"
|
|
"--lang=en-US"
|
|
"--window-size=1920,1080"
|
|
"--window-position=0,0"
|
|
"--no-first-run"
|
|
"--no-default-browser-check"
|
|
];
|
|
|
|
# Exec'd by i3 once the X session is up.
|
|
initScript = pkgs.writeShellScript "computer-use-init" ''
|
|
set -eu
|
|
|
|
# Make the X session visible to the systemd/dbus user manager, so MCP
|
|
# servers and other user services started later inherit DISPLAY.
|
|
export DISPLAY=${display}
|
|
${pkgs.dbus}/bin/dbus-update-activation-environment --systemd \
|
|
DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP || true
|
|
|
|
# This is an X11 session: drop any stale Wayland variables (left over from
|
|
# a previous session), otherwise tools such as x11vnc refuse to start.
|
|
${pkgs.systemd}/bin/systemctl --user unset-environment WAYLAND_DISPLAY || true
|
|
|
|
# Enable the AT-SPI bridge so computer-use agents can read accessibility
|
|
# trees. at-spi-bus-launcher derives org.a11y.Status.IsEnabled from this key.
|
|
GSETTINGS_BACKEND=dconf \
|
|
DBUS_SESSION_BUS_ADDRESS="unix:path=''${XDG_RUNTIME_DIR}/bus" \
|
|
${pkgs.glib.bin}/bin/gsettings set org.gnome.desktop.interface toolkit-accessibility true \
|
|
>/dev/null 2>&1 || true
|
|
|
|
${lib.optionalString cfg.cuaDriver ''
|
|
# cua-driver sends content-free telemetry by default; turn it off.
|
|
${pkgs.cua-driver}/bin/cua-driver telemetry disable >/dev/null 2>&1 || true
|
|
''}
|
|
'';
|
|
in
|
|
{
|
|
options.modules.desktop.computerUse = {
|
|
enable = lib.mkEnableOption "headless X11/i3 session for computer use";
|
|
vnc = lib.mkEnableOption "a localhost-only x11vnc server for manual access";
|
|
cuaDriver = lib.mkEnableOption "the trycua/cua driver (cua-driver)";
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
home.packages =
|
|
with pkgs;
|
|
[
|
|
glib # gsettings, to enable the AT-SPI bridge
|
|
gsettings-desktop-schemas # the toolkit-accessibility schema
|
|
i3
|
|
kitty # terminal (supports X11 and Wayland)
|
|
xdotool # XTEST input (fallback / manual use)
|
|
xclip # clipboard
|
|
autocutsel # keep the X PRIMARY and CLIPBOARD selections in sync
|
|
xvfb # headless X server
|
|
]
|
|
++ [
|
|
# MCP server / CLI that drives the desktop (see overlays/nur-ryan4yin.nix).
|
|
pkgs.computer-use-linux
|
|
]
|
|
++ lib.optional cfg.vnc pkgs.x11vnc
|
|
++ lib.optional cfg.cuaDriver pkgs.cua-driver;
|
|
|
|
# Headless X server.
|
|
systemd.user.services.xvfb = {
|
|
Unit = {
|
|
Description = "Xvfb headless X server";
|
|
};
|
|
Install.WantedBy = [ "default.target" ];
|
|
Service = {
|
|
Type = "simple";
|
|
ExecStart = "${pkgs.xvfb}/bin/Xvfb ${display} -screen 0 ${screen} -nolisten tcp";
|
|
Restart = "always";
|
|
RestartSec = 2;
|
|
};
|
|
};
|
|
|
|
# Window manager.
|
|
systemd.user.services.i3 = {
|
|
Unit = {
|
|
Description = "i3 window manager (headless X)";
|
|
After = [ "xvfb.service" ];
|
|
Requires = [ "xvfb.service" ];
|
|
};
|
|
Install.WantedBy = [ "default.target" ];
|
|
Service = {
|
|
Type = "simple";
|
|
Environment = [
|
|
"DISPLAY=${display}"
|
|
"XDG_SESSION_TYPE=x11"
|
|
"XDG_CURRENT_DESKTOP=i3"
|
|
];
|
|
ExecStart = "${pkgs.i3}/bin/i3";
|
|
Restart = "always";
|
|
RestartSec = 2;
|
|
};
|
|
};
|
|
|
|
# Xvfb has no clipboard of its own, and an X11 selection lives only as long
|
|
# as the app that owns it. Mirror PRIMARY and CLIPBOARD with two autocutsel
|
|
# instances so a VNC client's clipboard (which syncs a single selection)
|
|
# stays usable in both directions.
|
|
systemd.user.services.autocutsel-primary = {
|
|
Unit = {
|
|
Description = "autocutsel (PRIMARY)";
|
|
After = [ "xvfb.service" ];
|
|
Wants = [ "xvfb.service" ];
|
|
};
|
|
Install.WantedBy = [ "default.target" ];
|
|
Service = {
|
|
Type = "simple";
|
|
Environment = [ "DISPLAY=${display}" ];
|
|
ExecStart = "${pkgs.autocutsel}/bin/autocutsel -selection PRIMARY";
|
|
Restart = "always";
|
|
RestartSec = 2;
|
|
};
|
|
};
|
|
|
|
systemd.user.services.autocutsel-clipboard = {
|
|
Unit = {
|
|
Description = "autocutsel (CLIPBOARD)";
|
|
After = [ "xvfb.service" ];
|
|
Wants = [ "xvfb.service" ];
|
|
};
|
|
Install.WantedBy = [ "default.target" ];
|
|
Service = {
|
|
Type = "simple";
|
|
Environment = [ "DISPLAY=${display}" ];
|
|
ExecStart = "${pkgs.autocutsel}/bin/autocutsel -selection CLIPBOARD";
|
|
Restart = "always";
|
|
RestartSec = 2;
|
|
};
|
|
};
|
|
|
|
# Optional VNC server for manual access. Localhost only: connect through an
|
|
# SSH tunnel (Remmina's VNC profile has an SSH Tunnel tab; plain
|
|
# `ssh -L 5900:127.0.0.1:5900` works too), so the port is never exposed and
|
|
# no VNC password is needed.
|
|
systemd.user.services.x11vnc = lib.mkIf cfg.vnc {
|
|
Unit = {
|
|
Description = "x11vnc (localhost) for manual access";
|
|
After = [ "xvfb.service" ];
|
|
Wants = [ "xvfb.service" ];
|
|
};
|
|
Install.WantedBy = [ "default.target" ];
|
|
Service = {
|
|
Type = "simple";
|
|
ExecStart = "${pkgs.x11vnc}/bin/x11vnc -display ${display} -localhost -nopw -forever -shared -rfbport 5900";
|
|
Restart = "always";
|
|
RestartSec = 2;
|
|
};
|
|
};
|
|
|
|
# cua-driver daemon, so `cua-driver mcp` / `call` can reach it.
|
|
#
|
|
# Deliberately a plain `serve` with no launch grants. The driver must drive
|
|
# the browser with real X11/AT-SPI input, never its CDP-backed `browser_*`
|
|
# tools: attaching to the existing profile needs `--grant existing-profile`
|
|
# (or unrestricted mode) and turns on a persistent, detectable
|
|
# remote-debugging endpoint. Do not add one here. See README.md.
|
|
systemd.user.services.cua-driver = lib.mkIf cfg.cuaDriver {
|
|
Unit = {
|
|
Description = "cua-driver daemon";
|
|
After = [
|
|
"xvfb.service"
|
|
"i3.service"
|
|
];
|
|
Wants = [ "xvfb.service" ];
|
|
};
|
|
Install.WantedBy = [ "default.target" ];
|
|
Service = {
|
|
Type = "simple";
|
|
Environment = [
|
|
"DISPLAY=${display}"
|
|
"XDG_SESSION_TYPE=x11"
|
|
];
|
|
ExecStart = "${pkgs.cua-driver}/bin/cua-driver serve";
|
|
Restart = "always";
|
|
RestartSec = 2;
|
|
};
|
|
};
|
|
|
|
xdg.configFile."i3/config".text = ''
|
|
# i3 configuration for the headless computer-use session.
|
|
# Keep it minimal; see README.md in this directory for the rationale.
|
|
|
|
font pango:monospace 10
|
|
default_border normal
|
|
|
|
exec --no-startup-id ${initScript}
|
|
'';
|
|
|
|
programs.brave-origin = {
|
|
enable = true;
|
|
commandLineArgs = browserFlags;
|
|
};
|
|
programs.chromium = {
|
|
enable = true;
|
|
commandLineArgs = browserFlags;
|
|
};
|
|
};
|
|
}
|