Files
Panama/config/dot/hypr/autostart.lua
T
Gabriel Brown 116510caa8 Draw the authentication prompt ourselves
hyprpolkitagent's dialog is compiled into its binary -- no config, no
stylesheet, nothing to theme -- and it was the one window on this
desktop that looked like it belonged to something else.

The split between the two halves is the security design, not an
implementation detail. A small agent process owns the D-Bus side: it
registers with polkitd, receives the request, and hands the shell the
action, the message, who may answer, and a one-time cookie. It never
sees a password. The shell draws the prompt and, on submit, spawns the
setuid polkit-agent-helper-1 itself and writes the password to that
helper's stdin; the helper runs the PAM conversation and reports to
polkitd directly. The password exists in the shell and in the helper's
stdin and nowhere else -- never on a command line, never over D-Bus,
never through IPC arguments.

The prompt takes exclusive keyboard focus, because a password field that
lets keystrokes reach the window behind it is a keylogger with extra
steps. The request travels as a file created 0600 with O_EXCL inside a
0700 runtime directory: a cookie is not a password, but it is a
capability, and capabilities do not belong in a process listing either.

Three things cost real time. polkitd calls back on the same connection
that registered, so exporting the object on the session bus while
registering from the system bus failed every request as "Not authorized"
with no error anywhere. XDG_SESSION_ID is absent in a systemd user unit,
which runs under [email protected] and belongs to no login session, so the
session comes from logind's Display property instead. And PyGObject does
not accept the @ placeholder in variant format strings.

hyprpolkitagent stays installed as the fallback, only one agent is
started, and the comment beside the autostart says how to get the stock
prompt back. Verified end to end, including a real password accepted and
three cancellations refused.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-19 18:08:52 -04:00

78 lines
4.8 KiB
Lua

-- ─────────────────────────────────────────────────────────────────────────────
-- Autostart
--
-- `exec-once` no longer exists in 0.56; startup is an event handler.
--
-- Session: Fedora installs two sessions, "Hyprland" and "Hyprland
-- (uwsm-managed)". Panama targets the uwsm one, because
-- xdg-desktop-portal-hyprland's systemd unit requires graphical-session.target
-- and Fedora ships no hyprland-session.target -- without uwsm, screen sharing
-- in OBS/Sunshine/Zoom silently fails. The target is started explicitly below
-- so the plain session works too.
--
-- Anything that ships a systemd user unit is started as a unit rather than as
-- a compositor child. That is not cosmetic: under uwsm, units get the correct
-- activation environment (see uwsm/env), they restart on failure, and they
-- shut down in order. Panama's `change-settings` script enables them once;
-- the explicit starts here make a fresh clone work before that has run.
-- ─────────────────────────────────────────────────────────────────────────────
hl.on("hyprland.start", function()
-- Publish the session environment to systemd and D-Bus so user units and
-- the portals can see WAYLAND_DISPLAY. Cheap, and essential without uwsm.
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=Hyprland")
hl.exec_cmd("systemctl --user start hyprland-session.target")
-- Units: polkit prompts, wallpaper, launcher daemon, idle/lock. All four
-- carry `ConditionEnvironment=WAYLAND_DISPLAY`, and hl.exec_cmd fires
-- commands without waiting for them to finish, so the dbus-update call
-- above racing this one is not safe to assume complete -- a lost race
-- leaves the Condition unmet and the unit silently never starts (exit 0,
-- no error). hypridle is the only listener for the logind Lock signal,
-- so that failure mode is "lock-session goes to nobody". Re-import
-- synchronously in the same shell invocation first so the Condition
-- always sees it, regardless of how the dbus-update call above scheduled.
-- panama-polkit-agent replaces hyprpolkitagent, whose prompt is compiled
-- into its binary and cannot be themed. Only one agent may register per
-- session, so they must not both start. hyprpolkitagent stays INSTALLED as
-- the fallback: `systemctl --user start hyprpolkitagent` restores the stock
-- prompt if Panama's ever fails to come up.
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP && systemctl --user start panama-polkit-agent.service hyprpaper.service vicinae.service hypridle.service")
-- The shell: bar, dock, overview, quick settings, notifications, capture.
-- No systemd unit ships with quickshell, so it runs as a compositor child.
hl.exec_cmd("quickshell --daemonize")
-- Removable-media automounting. GNOME did this invisibly via gvfs+udisks;
-- outside GNOME something has to ask udisks to mount. No tray icon: the
-- Quickshell bar already has a tray, and udiskie's own icon would be
-- redundant. -f opens Nautilus when you click the mount notification.
hl.exec_cmd("udiskie --automount --notify --no-tray --file-manager nautilus")
-- Keyring unlock, for Nextcloud and Bitwarden credential storage.
hl.exec_cmd("/usr/bin/gnome-keyring-daemon --start --components=secrets,ssh,pkcs11")
-- Nextcloud and Bitwarden are NOT started here. Both ship a
-- ~/.config/autostart/*.desktop entry, and systemd's own
-- systemd-xdg-autostart-generator turns every such entry into a
-- `PartOf=graphical-session.target` unit (`[email protected]`,
-- `[email protected]`) that fires once the uwsm
-- session brings up graphical-session.target -- confirmed live via
-- `systemctl --user list-units 'app-*@autostart.service'`. An explicit
-- second launch here used to duplicate that: for Bitwarden specifically,
-- each `flatpak run` gets its own sandbox instance, so the two starts
-- didn't just race, they left two competing processes fighting over the
-- app's single-instance lock, with neither reliably owning a usable
-- window. Trust the generator instead of re-launching.
--
-- RustDesk is deliberately absent too: it ships an enabled *system*
-- service (`rustdesk --service`) that spawns --server and --tray for the
-- session on its own. Starting it here as well would give you two trays.
end)
hl.on("hyprland.shutdown", function()
hl.exec_cmd("systemctl --user stop hyprland-session.target")
end)
return true