git.delta.rocks / fleet / refs/commits / 0ea4b47ffa04

difftreelog

refactor drop lach.PolkitHelper

tkwkzmxpYaroslav Bolyukin2026-06-18parent: #0879385.patch.diff
Polkit127 has introduced /run/polkit/agent-helper.socket instead, which
implements the same thing I want.

18 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -560,31 +560,11 @@
 
 [[package]]
 name = "bindgen"
-version = "0.69.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
-dependencies = [
- "bitflags 2.13.0",
- "cexpr",
- "clang-sys",
- "itertools 0.12.1",
- "lazy_static",
- "lazycell",
- "proc-macro2",
- "quote",
- "regex",
- "rustc-hash 1.1.0",
- "shlex 1.3.0",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "bindgen"
 version = "0.72.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "cexpr",
  "clang-sys",
  "itertools 0.13.0",
@@ -597,12 +577,6 @@
  "shlex 1.3.0",
  "syn 2.0.118",
 ]
-
-[[package]]
-name = "bitflags"
-version = "1.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
@@ -1415,7 +1389,7 @@
 checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
 dependencies = [
  "data-encoding",
- "syn 1.0.109",
+ "syn 2.0.118",
 ]
 
 [[package]]
@@ -1566,7 +1540,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "block2",
  "libc",
  "objc2",
@@ -1707,17 +1681,6 @@
 ]
 
 [[package]]
-name = "enum-repr"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bad30c9c0fa1aaf1ae5010dab11f1117b15d35faf62cda4bbbc53b9987950f18"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
 name = "enum_dispatch"
 version = "0.3.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3125,15 +3088,6 @@
 
 [[package]]
 name = "itertools"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
-
-[[package]]
-name = "itertools"
 version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
@@ -3288,12 +3242,6 @@
 dependencies = [
  "spin 0.9.8",
 ]
-
-[[package]]
-name = "lazycell"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
 
 [[package]]
 name = "libc"
@@ -3323,7 +3271,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "libc",
  "plain",
  "redox_syscall 0.8.1",
@@ -3611,7 +3559,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "df9854ea6ad14e3f4698a7f03b65bce0833dd2d81d594a0e4a984170537146b6"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "libc",
  "log",
  "netlink-packet-core",
@@ -3623,7 +3571,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2288fcb784eb3defd5fb16f4c4160d5f477de192eac730f43e1d11c24d9a007"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "libc",
  "log",
  "netlink-packet-core",
@@ -3699,7 +3647,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "cfg-if",
  "cfg_aliases",
  "libc",
@@ -3710,7 +3658,7 @@
 version = "0.1.9"
 dependencies = [
  "anyhow",
- "bindgen 0.72.1",
+ "bindgen",
  "camino",
  "cxx",
  "cxx-build",
@@ -3943,7 +3891,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "block2",
  "dispatch2",
  "libc",
@@ -3956,7 +3904,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "objc2",
  "objc2-core-foundation",
  "objc2-foundation",
@@ -3976,7 +3924,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "block2",
  "libc",
  "objc2",
@@ -3989,7 +3937,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "objc2",
  "objc2-core-foundation",
 ]
@@ -4010,7 +3958,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "dispatch2",
  "libc",
  "objc2",
@@ -4215,30 +4163,6 @@
  "tokio",
  "windows",
  "windows-strings",
-]
-
-[[package]]
-name = "pam-client"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51bd776116a7ada5ebbe31f54cdc5b1030ed7265686cf7c8a21c057a2f8dab9a"
-dependencies = [
- "bitflags 1.3.2",
- "enum-repr",
- "libc",
- "pam-sys",
- "rpassword",
- "rustversion",
-]
-
-[[package]]
-name = "pam-sys"
-version = "1.0.0-alpha5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b"
-dependencies = [
- "bindgen 0.69.5",
- "libc",
 ]
 
 [[package]]
@@ -4507,23 +4431,6 @@
  "quick-xml",
  "serde",
  "time",
-]
-
-[[package]]
-name = "polkit-backend"
-version = "0.1.9"
-dependencies = [
- "anyhow",
- "clap",
- "nix",
- "pam-client",
- "remowt-polkit-shared",
- "remowt-ui-prompt",
- "tokio",
- "tracing",
- "tracing-subscriber",
- "zbus",
- "zbus_polkit",
 ]
 
 [[package]]
@@ -4761,7 +4668,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "num-traits",
  "rand 0.9.4",
  "rand_chacha 0.9.0",
@@ -4988,7 +4895,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
 ]
 
 [[package]]
@@ -4997,7 +4904,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
 ]
 
 [[package]]
@@ -5260,18 +5167,6 @@
  "libc",
  "untrusted",
  "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "rpassword"
-version = "6.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bf099a1888612545b683d2661a1940089f6c2e5a8e38979b2159da876bfd956"
-dependencies = [
- "libc",
- "serde",
- "serde_json",
- "winapi",
 ]
 
 [[package]]
@@ -5320,7 +5215,7 @@
 checksum = "bbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0"
 dependencies = [
  "aes 0.9.1",
- "bitflags 2.13.0",
+ "bitflags",
  "block-padding 0.4.2",
  "byteorder",
  "bytes",
@@ -5484,7 +5379,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "errno",
  "libc",
  "linux-raw-sys 0.4.15",
@@ -5497,7 +5392,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "errno",
  "libc",
  "linux-raw-sys 0.12.1",
@@ -5700,7 +5595,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "core-foundation 0.10.1",
  "core-foundation-sys",
  "libc",
@@ -5956,7 +5851,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
 ]
 
 [[package]]
@@ -6184,7 +6079,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "core-foundation 0.9.4",
  "system-configuration-sys",
 ]
@@ -6610,7 +6505,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
 dependencies = [
- "bitflags 2.13.0",
+ "bitflags",
  "bytes",
  "futures-util",
  "http",
@@ -6953,7 +6848,7 @@
 checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
 dependencies = [
  "arrayvec",
- "bitflags 2.13.0",
+ "bitflags",
  "cursor-icon",
  "log",
  "memchr",
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -89,7 +89,6 @@
 zbus = "5.16.0"
 zbus_polkit = "5.0.0"
 tokio-util = "0.7.11"
-pam-client = "0.5.0"
 
 [profile.dev]
 panic = "abort"
deletedremowt/cmds/polkit-dbus-helper/Cargo.tomldiffbeforeafterboth
--- a/remowt/cmds/polkit-dbus-helper/Cargo.toml
+++ /dev/null
@@ -1,19 +0,0 @@
-[package]
-name = "polkit-backend"
-description = "polkit/PAM D-Bus authentication backend for remowt"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-anyhow.workspace = true
-clap = { workspace = true, features = ["derive"] }
-nix.workspace = true
-pam-client.workspace = true
-remowt-polkit-shared.workspace = true
-tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }
-tracing.workspace = true
-tracing-subscriber.workspace = true
-remowt-ui-prompt.workspace = true
-zbus = { workspace = true, features = ["tokio"] }
-zbus_polkit = { workspace = true, features = ["tokio"] }
deletedremowt/cmds/polkit-dbus-helper/README.adocdiffbeforeafterboth
--- a/remowt/cmds/polkit-dbus-helper/README.adoc
+++ /dev/null
@@ -1,8 +0,0 @@
-== What is it?
-
-Usually, there is a `polkit-agent-helper-1` suid binary installed on the system with polkit package.
-
-This, however, an alternative to that approach, a system daemon listening for dbus requests, which works
-without using suid binaries.
-
-In future it will provide some additional features.
deletedremowt/cmds/polkit-dbus-helper/etc/systemd/system/remowt-polkit-helper.servicediffbeforeafterboth
--- a/remowt/cmds/polkit-dbus-helper/etc/systemd/system/remowt-polkit-helper.service
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-Description=Remowt polkit helper service
-
-[Service]
-Type=dbus
-BusName=lach.polkit.helper1
-ExecStart=@libexecdir@/polkit-backend
-# TODO: Hardening
-
-[Install]
-WantedBy=multi-user.target
-Alias=dbus-lach.polkit.helper1.service
deletedremowt/cmds/polkit-dbus-helper/share/dbus-1/system-services/lach.polkit.helper1.confdiffbeforeafterboth
--- a/remowt/cmds/polkit-dbus-helper/share/dbus-1/system-services/lach.polkit.helper1.conf
+++ /dev/null
@@ -1,5 +0,0 @@
-[D-BUS Service]
-Name=lach.polkit.helper1
-Exec=/bin/false
-User=root
-SystemdService=dbus-lach.polkit.helper1.service
deletedremowt/cmds/polkit-dbus-helper/share/dbus-1/system.d/lach.polkit.helper1.confdiffbeforeafterboth
--- a/remowt/cmds/polkit-dbus-helper/share/dbus-1/system.d/lach.polkit.helper1.conf
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
-<busconfig>
-	<policy user="root">
-		<allow own = "lach.polkit.helper1"/>
-		<allow send_interface="lach.PolkitInputHandler"/>
-	</policy>
-	<policy context="default">
-		<allow send_destination="lach.polkit.helper1"/>
-		<deny send_interface="lach.PolkitInputHandler"/>
-	</policy>
-</busconfig>
deletedremowt/cmds/polkit-dbus-helper/src/main.rsdiffbeforeafterboth
--- a/remowt/cmds/polkit-dbus-helper/src/main.rs
+++ /dev/null
@@ -1,236 +0,0 @@
-use std::collections::{HashMap, HashSet};
-use std::ffi::{CStr, CString};
-use std::future::pending;
-use std::sync::LazyLock;
-
-use anyhow::Context as _;
-use clap::Parser;
-use nix::unistd::{setuid, Uid, User};
-use pam_client::{Context, ConversationHandler, ErrorCode, Flag};
-use remowt_polkit_shared::BackendRequest;
-use remowt_ui_prompt::dbus::DbusPrompterProxyBlocking;
-use remowt_ui_prompt::BlockingPrompter;
-use tokio::task::{block_in_place, spawn_blocking};
-use tracing::trace;
-use zbus::fdo;
-use zbus::message::Header;
-use zbus::zvariant::OwnedValue;
-use zbus::{blocking, interface, proxy, Connection};
-
-struct Helper {
-	connection: Connection,
-	blocking_connection: blocking::Connection,
-}
-
-static ALLOWED_ENVIRONMENT: LazyLock<HashSet<&str>> = LazyLock::new(|| {
-	[
-		// pam ssh agent auth
-		"SSH_AUTH_SOCK",
-		// ssh itself provides this when running PAM
-		"SSH_AUTH_INFO_0",
-		// contains user which ran sudo
-		"SUDO_USER",
-	]
-	.into_iter()
-	.collect()
-});
-
-struct Conversation<P>(P);
-impl<P: BlockingPrompter> Conversation<P> {
-	fn prompt_inner(&self, echo: bool, prompt: &CStr) -> Result<CString, ErrorCode> {
-		trace!("do prompt");
-		let out = self
-			.0
-			.prompt_text(echo, &prompt.to_string_lossy(), "PAM prompt request", &[])
-			.map_err(|e| {
-				trace!("prompt error: {e}");
-				ErrorCode::CONV_ERR
-			})?;
-		CString::new(out).map_err(|_| ErrorCode::CONV_AGAIN)
-	}
-	fn text_inner(&self, error: bool, msg: &CStr) {
-		trace!("do text");
-		let msg = msg.to_string_lossy();
-		let _ = self.0.display_text(error, &msg, &[]);
-	}
-}
-impl<P: BlockingPrompter> ConversationHandler for Conversation<P> {
-	fn prompt_echo_on(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {
-		self.prompt_inner(true, prompt)
-	}
-
-	fn prompt_echo_off(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {
-		self.prompt_inner(false, prompt)
-	}
-
-	fn text_info(&mut self, msg: &CStr) {
-		self.text_inner(false, msg)
-	}
-
-	fn error_msg(&mut self, msg: &CStr) {
-		self.text_inner(true, msg)
-	}
-
-	fn radio_prompt(&mut self, prompt: &CStr) -> Result<bool, ErrorCode> {
-		let prompt = prompt.to_string_lossy();
-		let result = self
-			.0
-			.prompt_radio(&prompt, "PAM prompt request", &[])
-			.map_err(|_| ErrorCode::CONV_ERR)?;
-		Ok(result)
-	}
-}
-
-#[proxy(
-	default_service = "org.freedesktop.DBus",
-	default_path = "/org/freedesktop/DBus"
-)]
-trait DBus {
-	fn get_connection_credentials(&self, body: &str) -> zbus::Result<HashMap<String, OwnedValue>>;
-}
-
-#[interface(name = "lach.PolkitHelper")]
-impl Helper {
-	async fn init_conversation(
-		&self,
-		request: BackendRequest,
-		#[zbus(header)] hdr: Header<'_>,
-	) -> fdo::Result<()> {
-		let Some(sender) = hdr.sender().map(|v| v.to_owned()) else {
-			trace!("missing sender");
-			return Err(fdo::Error::AuthFailed("missing sender".to_owned()));
-		};
-
-		let dbus = DBusProxy::new(&self.connection).await?;
-
-		// TOCTOU: sender might be already disconnected, and there might be another
-		// user with different user id here, but does it matters?
-		let reply = dbus.get_connection_credentials(&sender).await?;
-		let connection_uid: u32 = (&reply["UnixUserID"]).try_into().unwrap();
-
-		let identity = request.identity.clone();
-		let blocking_connection = self.blocking_connection.clone();
-		let thread_result: fdo::Result<()> = block_in_place(move || {
-			trace!("find user");
-			let Some(identity_uid) = identity.uid() else {
-				return Err(fdo::Error::AuthFailed("can't process identity".to_owned()));
-			};
-			let user = User::from_uid(identity_uid)
-				.map_err(|_| fdo::Error::AuthFailed("error querying user".to_owned()))?
-				.ok_or_else(|| fdo::Error::AuthFailed("uid not found".to_owned()))?;
-
-			let responder = DbusPrompterProxyBlocking::new(
-				&blocking_connection,
-				sender,
-				request.prompter_path,
-			)?;
-			let conversation = Conversation(responder);
-			trace!("run context for {}", &user.name);
-			let mut ctx = Context::new(
-				// TODO: Should another scope be used?
-				"login",
-				Some(&user.name),
-				conversation,
-			)
-			.map_err(|_| fdo::Error::Failed("pam context init failed".to_owned()))?;
-
-			trace!("fill env");
-			for (k, v) in request.environment {
-				if k.contains('=') || !ALLOWED_ENVIRONMENT.contains(k.as_str()) {
-					continue;
-				}
-				let _ = ctx.putenv(format!("{k}={v}"));
-			}
-
-			trace!("authenticate");
-			ctx.authenticate(Flag::NONE)
-				.map_err(|_| fdo::Error::AuthFailed("pam authentication failed".to_owned()))?;
-
-			trace!("acct mgmt");
-			ctx.acct_mgmt(Flag::NONE)
-				.map_err(|_| fdo::Error::AuthFailed("pam acct mgmt failed".to_owned()))?;
-
-			Ok(())
-		});
-
-		thread_result?;
-
-		trace!("respond");
-		let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&self.connection).await?;
-
-		let identity_details = request
-			.identity
-			.details
-			.iter()
-			.map(|(k, v)| (k.as_str(), (**v).try_clone().expect("success")))
-			.collect::<HashMap<_, _>>();
-		proxy
-			.authentication_agent_response2(
-				connection_uid,
-				&request.cookie,
-				&zbus_polkit::policykit1::Identity {
-					identity_kind: &request.identity.kind,
-					identity_details: &identity_details,
-				},
-			)
-			.await?;
-		Ok(())
-	}
-}
-
-const OBJ_PATH: &str = "/lach/PolkitHelper";
-
-#[derive(Parser)]
-struct Opts {
-	/// Not recommended: start as a session connection, then use escalation
-	/// to respond to polkit requests.
-	#[arg(long)]
-	session: bool,
-}
-
-#[tokio::main]
-async fn main() -> anyhow::Result<()> {
-	tracing_subscriber::fmt::init();
-	let opts = Opts::parse();
-	let connection = if opts.session {
-		Connection::session().await
-	} else {
-		Connection::system().await
-	}
-	.context("failed to open connection")?;
-
-	let session = opts.session;
-	let blocking_connection: anyhow::Result<blocking::Connection> = spawn_blocking(move || {
-		Ok(if session {
-			blocking::Connection::session()?
-		} else {
-			blocking::Connection::system()?
-		})
-	})
-	.await?;
-	let blocking_connection = blocking_connection.context("failed to open blocking connection")?;
-
-	if opts.session {
-		setuid(Uid::from_raw(0))
-			.context("polkit-backend needs to be suid if run in session mode")?;
-	}
-
-	connection
-		.object_server()
-		.at(
-			OBJ_PATH,
-			Helper {
-				connection: connection.clone(),
-				blocking_connection,
-			},
-		)
-		.await
-		.context("failed listen path")?;
-
-	connection
-		.request_name("lach.polkit.helper1")
-		.await
-		.context("failed to request name")?;
-
-	pending().await
-}
deletedremowt/cmds/remowt-agent/src/helper/dbus.rsdiffbeforeafterboth
--- a/remowt/cmds/remowt-agent/src/helper/dbus.rs
+++ /dev/null
@@ -1,81 +0,0 @@
-use std::collections::HashMap;
-use std::marker::PhantomData;
-
-use remowt_polkit_shared::{BackendRequest, Identity};
-use remowt_ui_prompt::dbus::DbusPrompterInterface;
-use remowt_ui_prompt::Prompter;
-use zbus::Connection;
-
-use crate::PolkitHelperProxy;
-
-use super::Helper;
-
-struct TemporaryPrompterInterface<P: Prompter + 'static> {
-	connection: Connection,
-	path: String,
-	_marker: PhantomData<P>,
-}
-impl<P: Prompter + 'static> TemporaryPrompterInterface<P> {
-	async fn new(connection: Connection, prompter: P) -> Self {
-		let path = format!(
-			"/remowt/prompters/{}",
-			uuid::Uuid::new_v4().to_string().replace("-", "_")
-		);
-		let _ = connection
-			.object_server()
-			.at(path.clone(), DbusPrompterInterface(prompter))
-			.await;
-		Self {
-			connection,
-			path,
-			_marker: PhantomData,
-		}
-	}
-}
-impl<P: Prompter + Send + Sync + 'static> Drop for TemporaryPrompterInterface<P> {
-	fn drop(&mut self) {
-		// Removal is async because of async RwLock used inside...
-		// We should not care about its reuse
-		let connection = self.connection.clone();
-		let path = std::mem::take(&mut self.path);
-		tokio::spawn(async move {
-			let _ = connection
-				.object_server()
-				.remove::<DbusPrompterInterface<P>, String>(path)
-				.await;
-		});
-	}
-}
-
-#[derive(Clone)]
-pub struct DbusHelper {
-	connection: Connection,
-	helper: PolkitHelperProxy<'static>,
-}
-impl DbusHelper {
-	pub async fn new(connection: Connection) -> zbus::Result<Self> {
-		let helper = PolkitHelperProxy::new(&connection).await?;
-		Ok(Self { connection, helper })
-	}
-}
-impl Helper for DbusHelper {
-	async fn help_me<P: Prompter + Send + Sync + 'static>(
-		&self,
-		cookie: &str,
-		prompter: P,
-		identity: Identity,
-	) -> anyhow::Result<()> {
-		let prompter = TemporaryPrompterInterface::new(self.connection.clone(), prompter).await;
-		self.helper
-			.init_conversation(
-				BackendRequest {
-					cookie: cookie.to_owned(),
-					environment: HashMap::new(),
-					prompter_path: prompter.path.clone(),
-					identity,
-				}, // cookie.to_owned(), HashMap::new(), prompter.path.clone()
-			)
-			.await?;
-		Ok(())
-	}
-}
modifiedremowt/cmds/remowt-agent/src/helper/mod.rsdiffbeforeafterboth
--- a/remowt/cmds/remowt-agent/src/helper/mod.rs
+++ b/remowt/cmds/remowt-agent/src/helper/mod.rs
@@ -2,12 +2,10 @@
 use remowt_polkit_shared::Identity;
 use remowt_ui_prompt::Prompter;
 
-mod dbus;
 mod protocol;
 mod socket;
 mod suid;
 
-pub use dbus::DbusHelper;
 pub use socket::SocketHelper;
 pub use suid::SuidHelper;
 
modifiedremowt/cmds/remowt-agent/src/main.rsdiffbeforeafterboth
before · remowt/cmds/remowt-agent/src/main.rs
1use std::borrow::Cow;2use std::collections::{BTreeMap, HashMap};3use std::fs::Permissions;4use std::future::pending;5use std::io;6use std::os::unix::fs::PermissionsExt as _;7use std::path::PathBuf;8use std::sync::{Arc, Mutex, OnceLock};910use bifrostlink::declarative::RemoteEndpoints;11use bifrostlink::Rpc;12use bifrostlink_ports::stdio::from_stdio;13use bifrostlink_ports::unix_socket::from_socket;14use clap::Parser;15use remowt_endpoints::{16	forward::Forward, fs::Fs, iroh_tunnel::IrohTunnel, nix_daemon::NixDaemon, pty::Pty,17	subprocess::Subprocess, systemd::Systemd,18};19use remowt_link_shared::iroh_tunnel::TunnelDialer;20use remowt_link_shared::{editor::EditorEndpointsClient, Address, BifConfig};21use remowt_polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};22use remowt_ui_prompt::bifrost::PromptEndpointsClient;23use remowt_ui_prompt::rofi::RofiPrompter;24use remowt_ui_prompt::{PrependSourcePrompter, Prompter, Source};25use tokio::fs;26use tokio::net::UnixStream;27use tokio::runtime::Builder;28use tokio::task::AbortHandle;29use tracing::{debug, trace};30use zbus::fdo;31use zbus::zvariant::{OwnedValue, Str};32use zbus::{interface, proxy, Connection};33use zbus_polkit::policykit1::Subject;3435use self::helper::{Helper, SocketHelper, SuidHelper};3637pub mod askpass;38pub mod bus;39pub mod editor;40pub mod helper;4142struct CancelTaskOnDrop {43	tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,44	handle: String,45}46impl Drop for CancelTaskOnDrop {47	fn drop(&mut self) {48		debug!("cancel on drop");49		if let Some(task) = self50			.tasks51			.lock()52			.expect("not poisoned")53			.remove(&self.handle)54		{55			task.abort();56		}57	}58}5960struct Agent<H, P> {61	tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,62	helper: H,63	prompter: P,64}65impl<H, P> Agent<H, P> {66	fn new(helper: H, prompter: P) -> Self {67		Agent {68			tasks: Arc::new(Mutex::new(HashMap::new())),69			helper,70			prompter,71		}72	}73}7475#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]76impl<H, P> Agent<H, P>77where78	H: Helper + Clone + Send + Sync + 'static,79	P: Prompter + Clone + Send + Sync + 'static,80{81	/// BeginAuthentication method82	#[allow(clippy::too_many_arguments)]83	async fn begin_authentication(84		&self,85		action_id: String,86		message: String,87		_icon_name: String,88		mut details: BTreeMap<String, String>,89		cookie: String,90		identities: Vec<Identity>,91	) -> zbus::fdo::Result<()> {92		use std::fmt::Write;93		debug!("begin auth");94		let _cancel_guard = Arc::new(OnceLock::new());95		let task = {96			let helper = self.helper.clone();97			let prompter = self.prompter.clone();98			let cookie = cookie.clone();99			let _cancel_guard = _cancel_guard.clone();100			tokio::task::spawn(async move {101				let _cancel_guard = _cancel_guard.clone();102				trace!("conversation task");103				let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);104				if let Some(subject) = details.remove("polkit.caller-pid") {105					let _ = write!(description, "\n<b>Caller:</b> ");106					if let Ok(pid) = subject.parse::<u32>() {107						let _ = write!(description, "{}", PidDisplay(pid));108					} else {109						let _ = write!(description, "{}", emphasize("invalid pid"));110					}111				}112				if let Some(subject) = details.remove("polkit.subject-pid") {113					let _ = write!(description, "\n<b>Subject:</b> ");114					if let Ok(pid) = subject.parse::<u32>() {115						let _ = write!(description, "{}", PidDisplay(pid));116					} else {117						let _ = write!(description, "{}", emphasize("invalid pid"));118					}119				}120				let mut prompter = PrependSourcePrompter {121					source: vec![Source(Cow::Borrowed("polkit agent"))],122					description: description.clone(),123					prompter,124				};125126				let identity_displays: Vec<String> =127					identities.iter().map(|v| v.to_string()).collect();128				let identity_displays: Vec<&str> =129					identity_displays.iter().map(|v| v.as_str()).collect();130				debug!("choose identity");131				let choosen_identity = match identity_displays.len() {132					0 => {133						return Err(fdo::Error::AuthFailed(134							"no identity to authenticate as".to_owned(),135						))136					}137					1 => 0,138					_ => {139						prompter140							.prompt_enum(141								"Identity",142								"Select identity to use for polkit authorization",143								&identity_displays,144								&[],145							)146							.await?147					}148				};149				debug!("identity chosen");150151				let _ = write!(152					description,153					"\n<b>Identity:</b> {}",154					identities[choosen_identity as usize]155				);156				prompter.description = description;157158				prompter.source.push(Source(Cow::Borrowed("polkit daemon")));159160				helper161					.help_me(162						&cookie,163						prompter,164						identities[choosen_identity as usize].clone(),165					)166					.await167					.map_err(|e| fdo::Error::Failed(e.to_string()))?;168				// let connection = Connection::system().await?;169				// let helper = PolkitHelperProxy::new(&connection).await?;170171				Ok(())172			})173		};174		self.tasks175			.lock()176			.unwrap()177			.insert(cookie.clone(), task.abort_handle());178		debug!("abort handle stored");179		let _ = _cancel_guard.set(CancelTaskOnDrop {180			tasks: self.tasks.clone(),181			handle: cookie.clone(),182		});183184		let _ = task.await;185186		Ok(())187	}188189	/// CancelAuthentication method190	async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {191		debug!("auth cancelled");192		if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {193			debug!("abort handle found");194			abort.abort();195		}196		// debug!("Authentication cancled ! {cookie}");197		Ok(())198	}199}200201const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";202203#[proxy(204	interface = "lach.PolkitHelper",205	default_service = "lach.polkit.helper1",206	default_path = "/lach/PolkitHelper"207)]208trait PolkitHelper {209	fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;210}211212#[derive(Parser)]213enum Opts {214	AskPass {215		prompt: String,216		description: String,217	},218	Editor {219		/// Argument to nvim220		path: String,221	},222	/// Expose a remote TCP/UDP address on the local (User) machine, printing the local port.223	Forward {224		/// Protocol: `tcp` (ssh or iroh) or `udp` (iroh-only).225		proto: ForwardProto,226		/// Remote socket address to expose locally, e.g. `127.0.0.1:8080`.227		addr: String,228	},229	RealAgent {230		#[arg(long)]231		path: Option<PathBuf>,232		/// Expect own address to be AgentPrivileged, skip installing polkit agent233		#[arg(long)]234		privileged: bool,235		#[arg(long)]236		local: bool,237	},238	LocalAgent,239}240241#[derive(Clone, Copy, clap::ValueEnum)]242enum ForwardProto {243	Tcp,244	Udp,245}246247fn main() -> anyhow::Result<()> {248	tracing_subscriber::fmt()249		.with_writer(io::stderr)250		.without_time()251		.init();252	let opts = Opts::parse();253254	let runtime = Builder::new_current_thread().enable_all().build()?;255256	match opts {257		Opts::AskPass {258			prompt,259			description,260		} => runtime.block_on(askpass::ask(&prompt, description)),261		Opts::LocalAgent => runtime.block_on(main_real()),262		Opts::Editor { path } => runtime.block_on(editor::edit(path)),263		Opts::Forward { proto, addr } => {264			runtime.block_on(editor::forward(matches!(proto, ForwardProto::Udp), addr))265		}266		Opts::RealAgent {267			path,268			privileged,269			local,270		} => runtime.block_on(main_real_agent(path, privileged, local)),271	}272}273async fn main_real() -> anyhow::Result<()> {274	let system_conn = Connection::system().await?;275	let helper = SocketHelper {276		fallback: SuidHelper,277	};278	register_auth_agent(&system_conn, Agent::new(helper, RofiPrompter)).await?;279280	let session_conn = Connection::session().await?;281	askpass::serve(&session_conn, RofiPrompter).await?;282283	let _keep_alive = (system_conn, session_conn);284	pending().await285}286async fn main_real_agent(287	path: Option<PathBuf>,288	privileged: bool,289	local: bool,290) -> anyhow::Result<()> {291	let address = if privileged {292		Address::AgentPrivileged293	} else {294		Address::Agent295	};296	let mut rpc = Rpc::<BifConfig>::new(address);297298	let dialer = Arc::new(TunnelDialer::new());299	Fs::new().register_endpoints(&mut rpc);300	Systemd.register_endpoints(&mut rpc);301	Pty::new(dialer.clone()).register_endpoints(&mut rpc);302	Subprocess::new(dialer.clone()).register_endpoints(&mut rpc);303	NixDaemon::new(dialer.clone()).register_endpoints(&mut rpc);304	IrohTunnel::new(dialer.clone()).register_endpoints(&mut rpc);305	Forward::new(dialer.clone()).register_endpoints(&mut rpc);306307	remowt_plugin::host::serve(&mut rpc);308309	let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));310	let editor_client = EditorEndpointsClient::wrap(rpc.remote(Address::User));311312	let bus = bus::spawn().await?;313	askpass::serve(&bus.conn, user_prompter.clone()).await?;314	editor::serve(&bus.conn, editor_client).await?;315316	let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;317	let exe = std::env::current_exe()?;318	let askpass_helper = helpers.path().join("remowt-askpass");319	let editor_helper = helpers.path().join("remowt-editor");320	let forward_helper = helpers.path().join("remowt-forward");321	{322		let script = format!(323			"#!/bin/sh\nexec {} ask-pass \"password\" \"$1\"\n",324			sh_quote(&exe.to_string_lossy())325		);326		fs::write(&askpass_helper, script).await?;327		fs::set_permissions(&askpass_helper, Permissions::from_mode(0o755)).await?;328	}329	{330		let script = format!(331			"#!/bin/sh\nexec {} editor \"$1\"\n",332			sh_quote(&exe.to_string_lossy())333		);334		fs::write(&editor_helper, script).await?;335		fs::set_permissions(&editor_helper, Permissions::from_mode(0o755)).await?;336	}337	{338		let script = format!(339			"#!/bin/sh\nexec {} forward \"$@\"\n",340			sh_quote(&exe.to_string_lossy())341		);342		fs::write(&forward_helper, script).await?;343		fs::set_permissions(&forward_helper, Permissions::from_mode(0o755)).await?;344	}345346	// Safety: Hoping tokio own threads won't read any of those...347	unsafe {348		prepend_path(helpers.path());349		std::env::set_var("SUDO_ASKPASS", &askpass_helper);350		std::env::set_var("SSH_ASKPASS", &askpass_helper);351		std::env::set_var("SSH_ASKPASS_REQUIRE", "force");352		std::env::set_var("EDITOR", &editor_helper);353		std::env::set_var("VISUAL", &editor_helper);354		std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &bus.address);355	}356357	let port = match path {358		Some(path) => from_socket(UnixStream::connect(path).await?),359		None => from_stdio(),360	};361	rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));362363	let polkit_conn = if !privileged && !local {364		// The unprivileged agent doubles as a polkit authentication agent so365		// `run0` (e.g. our own elevation) routes its prompt to the User over366		// bifrost instead of failing on a tty-less session.367		let conn = Connection::system().await?;368		let helper = SocketHelper {369			fallback: SuidHelper,370		};371		register_auth_agent(&conn, Agent::new(helper, user_prompter)).await?;372		Some(conn)373	} else {374		None375	};376377	let _keep_alive = (bus, helpers, polkit_conn);378	pending().await379}380381async fn register_auth_agent<H, P>(conn: &Connection, agent: Agent<H, P>) -> anyhow::Result<()>382where383	H: Helper + Clone + Send + Sync + 'static,384	P: Prompter + Clone + Send + Sync + 'static,385{386	let proxy = zbus_polkit::policykit1::AuthorityProxy::new(conn).await?;387	conn.object_server().at(OBJ_PATH, agent).await?;388389	let subject = auth_agent_subject()?;390	proxy391		.register_authentication_agent(&subject, "C", OBJ_PATH)392		.await?;393	debug!(kind = subject.subject_kind, "registered polkit agent");394	Ok(())395}396397fn auth_agent_subject() -> anyhow::Result<Subject> {398	let mut details = HashMap::new();399	if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {400		let val: OwnedValue = Str::from(session_id).into();401		details.insert("session-id".to_string(), val);402		return Ok(Subject {403			subject_kind: "unix-session".to_string(),404			subject_details: details,405		});406	}407408	details.insert("pid".to_string(), OwnedValue::from(std::process::id()));409	Ok(Subject {410		subject_kind: "unix-process".to_string(),411		subject_details: details,412	})413}414415fn sh_quote(s: &str) -> String {416	format!("'{}'", s.replace('\'', "'\\''"))417}418419/// Prepend `dir` to the process `PATH`.420///421/// # SAFETY422///423/// Same as `set_var`424unsafe fn prepend_path(dir: &std::path::Path) {425	let value = match std::env::var_os("PATH") {426		Some(existing) => {427			let mut v = dir.as_os_str().to_owned();428			v.push(":");429			v.push(existing);430			v431		}432		None => dir.as_os_str().to_owned(),433	};434	unsafe {435		std::env::set_var("PATH", value);436	}437}
after · remowt/cmds/remowt-agent/src/main.rs
1use std::borrow::Cow;2use std::collections::{BTreeMap, HashMap};3use std::fs::Permissions;4use std::future::pending;5use std::io;6use std::os::unix::fs::PermissionsExt as _;7use std::path::PathBuf;8use std::sync::{Arc, Mutex, OnceLock};910use bifrostlink::declarative::RemoteEndpoints;11use bifrostlink::Rpc;12use bifrostlink_ports::stdio::from_stdio;13use bifrostlink_ports::unix_socket::from_socket;14use clap::Parser;15use remowt_endpoints::{16	forward::Forward, fs::Fs, iroh_tunnel::IrohTunnel, nix_daemon::NixDaemon, pty::Pty,17	subprocess::Subprocess, systemd::Systemd,18};19use remowt_link_shared::iroh_tunnel::TunnelDialer;20use remowt_link_shared::{editor::EditorEndpointsClient, Address, BifConfig};21use remowt_polkit_shared::{emphasize, Identity, PidDisplay};22use remowt_ui_prompt::bifrost::PromptEndpointsClient;23use remowt_ui_prompt::rofi::RofiPrompter;24use remowt_ui_prompt::{PrependSourcePrompter, Prompter, Source};25use tokio::fs;26use tokio::net::UnixStream;27use tokio::runtime::Builder;28use tokio::task::AbortHandle;29use tracing::{debug, trace};30use zbus::fdo;31use zbus::zvariant::{OwnedValue, Str};32use zbus::{interface, Connection};33use zbus_polkit::policykit1::Subject;3435use self::helper::{Helper, SocketHelper, SuidHelper};3637pub mod askpass;38pub mod bus;39pub mod editor;40pub mod helper;4142struct CancelTaskOnDrop {43	tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,44	handle: String,45}46impl Drop for CancelTaskOnDrop {47	fn drop(&mut self) {48		debug!("cancel on drop");49		if let Some(task) = self50			.tasks51			.lock()52			.expect("not poisoned")53			.remove(&self.handle)54		{55			task.abort();56		}57	}58}5960struct Agent<H, P> {61	tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,62	helper: H,63	prompter: P,64}65impl<H, P> Agent<H, P> {66	fn new(helper: H, prompter: P) -> Self {67		Agent {68			tasks: Arc::new(Mutex::new(HashMap::new())),69			helper,70			prompter,71		}72	}73}7475#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]76impl<H, P> Agent<H, P>77where78	H: Helper + Clone + Send + Sync + 'static,79	P: Prompter + Clone + Send + Sync + 'static,80{81	/// BeginAuthentication method82	#[allow(clippy::too_many_arguments)]83	async fn begin_authentication(84		&self,85		action_id: String,86		message: String,87		_icon_name: String,88		mut details: BTreeMap<String, String>,89		cookie: String,90		identities: Vec<Identity>,91	) -> zbus::fdo::Result<()> {92		use std::fmt::Write;93		debug!("begin auth");94		let _cancel_guard = Arc::new(OnceLock::new());95		let task = {96			let helper = self.helper.clone();97			let prompter = self.prompter.clone();98			let cookie = cookie.clone();99			let _cancel_guard = _cancel_guard.clone();100			tokio::task::spawn(async move {101				let _cancel_guard = _cancel_guard.clone();102				trace!("conversation task");103				let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);104				if let Some(subject) = details.remove("polkit.caller-pid") {105					let _ = write!(description, "\n<b>Caller:</b> ");106					if let Ok(pid) = subject.parse::<u32>() {107						let _ = write!(description, "{}", PidDisplay(pid));108					} else {109						let _ = write!(description, "{}", emphasize("invalid pid"));110					}111				}112				if let Some(subject) = details.remove("polkit.subject-pid") {113					let _ = write!(description, "\n<b>Subject:</b> ");114					if let Ok(pid) = subject.parse::<u32>() {115						let _ = write!(description, "{}", PidDisplay(pid));116					} else {117						let _ = write!(description, "{}", emphasize("invalid pid"));118					}119				}120				let mut prompter = PrependSourcePrompter {121					source: vec![Source(Cow::Borrowed("polkit agent"))],122					description: description.clone(),123					prompter,124				};125126				let identity_displays: Vec<String> =127					identities.iter().map(|v| v.to_string()).collect();128				let identity_displays: Vec<&str> =129					identity_displays.iter().map(|v| v.as_str()).collect();130				debug!("choose identity");131				let choosen_identity = match identity_displays.len() {132					0 => {133						return Err(fdo::Error::AuthFailed(134							"no identity to authenticate as".to_owned(),135						))136					}137					1 => 0,138					_ => {139						prompter140							.prompt_enum(141								"Identity",142								"Select identity to use for polkit authorization",143								&identity_displays,144								&[],145							)146							.await?147					}148				};149				debug!("identity chosen");150151				let _ = write!(152					description,153					"\n<b>Identity:</b> {}",154					identities[choosen_identity as usize]155				);156				prompter.description = description;157158				prompter.source.push(Source(Cow::Borrowed("polkit daemon")));159160				helper161					.help_me(162						&cookie,163						prompter,164						identities[choosen_identity as usize].clone(),165					)166					.await167					.map_err(|e| fdo::Error::Failed(e.to_string()))?;168				// let connection = Connection::system().await?;169				// let helper = PolkitHelperProxy::new(&connection).await?;170171				Ok(())172			})173		};174		self.tasks175			.lock()176			.unwrap()177			.insert(cookie.clone(), task.abort_handle());178		debug!("abort handle stored");179		let _ = _cancel_guard.set(CancelTaskOnDrop {180			tasks: self.tasks.clone(),181			handle: cookie.clone(),182		});183184		let _ = task.await;185186		Ok(())187	}188189	/// CancelAuthentication method190	async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {191		debug!("auth cancelled");192		if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {193			debug!("abort handle found");194			abort.abort();195		}196		// debug!("Authentication cancled ! {cookie}");197		Ok(())198	}199}200201const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";202203#[derive(Parser)]204enum Opts {205	AskPass {206		prompt: String,207		description: String,208	},209	Editor {210		/// Argument to nvim211		path: String,212	},213	/// Expose a remote TCP/UDP address on the local (User) machine, printing the local port.214	Forward {215		/// Protocol: `tcp` (ssh or iroh) or `udp` (iroh-only).216		proto: ForwardProto,217		/// Remote socket address to expose locally, e.g. `127.0.0.1:8080`.218		addr: String,219	},220	RealAgent {221		#[arg(long)]222		path: Option<PathBuf>,223		/// Expect own address to be AgentPrivileged, skip installing polkit agent224		#[arg(long)]225		privileged: bool,226		#[arg(long)]227		local: bool,228	},229	LocalAgent,230}231232#[derive(Clone, Copy, clap::ValueEnum)]233enum ForwardProto {234	Tcp,235	Udp,236}237238fn main() -> anyhow::Result<()> {239	tracing_subscriber::fmt()240		.with_writer(io::stderr)241		.without_time()242		.init();243	let opts = Opts::parse();244245	let runtime = Builder::new_current_thread().enable_all().build()?;246247	match opts {248		Opts::AskPass {249			prompt,250			description,251		} => runtime.block_on(askpass::ask(&prompt, description)),252		Opts::LocalAgent => runtime.block_on(main_real()),253		Opts::Editor { path } => runtime.block_on(editor::edit(path)),254		Opts::Forward { proto, addr } => {255			runtime.block_on(editor::forward(matches!(proto, ForwardProto::Udp), addr))256		}257		Opts::RealAgent {258			path,259			privileged,260			local,261		} => runtime.block_on(main_real_agent(path, privileged, local)),262	}263}264async fn main_real() -> anyhow::Result<()> {265	let system_conn = Connection::system().await?;266	let helper = SocketHelper {267		fallback: SuidHelper,268	};269	register_auth_agent(&system_conn, Agent::new(helper, RofiPrompter)).await?;270271	let session_conn = Connection::session().await?;272	askpass::serve(&session_conn, RofiPrompter).await?;273274	let _keep_alive = (system_conn, session_conn);275	pending().await276}277async fn main_real_agent(278	path: Option<PathBuf>,279	privileged: bool,280	local: bool,281) -> anyhow::Result<()> {282	let address = if privileged {283		Address::AgentPrivileged284	} else {285		Address::Agent286	};287	let mut rpc = Rpc::<BifConfig>::new(address);288289	let dialer = Arc::new(TunnelDialer::new());290	Fs::new().register_endpoints(&mut rpc);291	Systemd.register_endpoints(&mut rpc);292	Pty::new(dialer.clone()).register_endpoints(&mut rpc);293	Subprocess::new(dialer.clone()).register_endpoints(&mut rpc);294	NixDaemon::new(dialer.clone()).register_endpoints(&mut rpc);295	IrohTunnel::new(dialer.clone()).register_endpoints(&mut rpc);296	Forward::new(dialer.clone()).register_endpoints(&mut rpc);297298	remowt_plugin::host::serve(&mut rpc);299300	let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));301	let editor_client = EditorEndpointsClient::wrap(rpc.remote(Address::User));302303	let bus = bus::spawn().await?;304	askpass::serve(&bus.conn, user_prompter.clone()).await?;305	editor::serve(&bus.conn, editor_client).await?;306307	let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;308	let exe = std::env::current_exe()?;309	let askpass_helper = helpers.path().join("remowt-askpass");310	let editor_helper = helpers.path().join("remowt-editor");311	let forward_helper = helpers.path().join("remowt-forward");312	{313		let script = format!(314			"#!/bin/sh\nexec {} ask-pass \"password\" \"$1\"\n",315			sh_quote(&exe.to_string_lossy())316		);317		fs::write(&askpass_helper, script).await?;318		fs::set_permissions(&askpass_helper, Permissions::from_mode(0o755)).await?;319	}320	{321		let script = format!(322			"#!/bin/sh\nexec {} editor \"$1\"\n",323			sh_quote(&exe.to_string_lossy())324		);325		fs::write(&editor_helper, script).await?;326		fs::set_permissions(&editor_helper, Permissions::from_mode(0o755)).await?;327	}328	{329		let script = format!(330			"#!/bin/sh\nexec {} forward \"$@\"\n",331			sh_quote(&exe.to_string_lossy())332		);333		fs::write(&forward_helper, script).await?;334		fs::set_permissions(&forward_helper, Permissions::from_mode(0o755)).await?;335	}336337	// Safety: Hoping tokio own threads won't read any of those...338	unsafe {339		prepend_path(helpers.path());340		std::env::set_var("SUDO_ASKPASS", &askpass_helper);341		std::env::set_var("SSH_ASKPASS", &askpass_helper);342		std::env::set_var("SSH_ASKPASS_REQUIRE", "force");343		std::env::set_var("EDITOR", &editor_helper);344		std::env::set_var("VISUAL", &editor_helper);345		std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &bus.address);346	}347348	let port = match path {349		Some(path) => from_socket(UnixStream::connect(path).await?),350		None => from_stdio(),351	};352	rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));353354	let polkit_conn = if !privileged && !local {355		// The unprivileged agent doubles as a polkit authentication agent so356		// `run0` (e.g. our own elevation) routes its prompt to the User over357		// bifrost instead of failing on a tty-less session.358		let conn = Connection::system().await?;359		let helper = SocketHelper {360			fallback: SuidHelper,361		};362		register_auth_agent(&conn, Agent::new(helper, user_prompter)).await?;363		Some(conn)364	} else {365		None366	};367368	let _keep_alive = (bus, helpers, polkit_conn);369	pending().await370}371372async fn register_auth_agent<H, P>(conn: &Connection, agent: Agent<H, P>) -> anyhow::Result<()>373where374	H: Helper + Clone + Send + Sync + 'static,375	P: Prompter + Clone + Send + Sync + 'static,376{377	let proxy = zbus_polkit::policykit1::AuthorityProxy::new(conn).await?;378	conn.object_server().at(OBJ_PATH, agent).await?;379380	let subject = auth_agent_subject()?;381	proxy382		.register_authentication_agent(&subject, "C", OBJ_PATH)383		.await?;384	debug!(kind = subject.subject_kind, "registered polkit agent");385	Ok(())386}387388fn auth_agent_subject() -> anyhow::Result<Subject> {389	let mut details = HashMap::new();390	if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {391		let val: OwnedValue = Str::from(session_id).into();392		details.insert("session-id".to_string(), val);393		return Ok(Subject {394			subject_kind: "unix-session".to_string(),395			subject_details: details,396		});397	}398399	details.insert("pid".to_string(), OwnedValue::from(std::process::id()));400	Ok(Subject {401		subject_kind: "unix-process".to_string(),402		subject_details: details,403	})404}405406fn sh_quote(s: &str) -> String {407	format!("'{}'", s.replace('\'', "'\\''"))408}409410/// Prepend `dir` to the process `PATH`.411///412/// # SAFETY413///414/// Same as `set_var`415unsafe fn prepend_path(dir: &std::path::Path) {416	let value = match std::env::var_os("PATH") {417		Some(existing) => {418			let mut v = dir.as_os_str().to_owned();419			v.push(":");420			v.push(existing);421			v422		}423		None => dir.as_os_str().to_owned(),424	};425	unsafe {426		std::env::set_var("PATH", value);427	}428}
modifiedremowt/crates/polkit-shared/src/lib.rsdiffbeforeafterboth
--- a/remowt/crates/polkit-shared/src/lib.rs
+++ b/remowt/crates/polkit-shared/src/lib.rs
@@ -103,11 +103,3 @@
 		}
 	}
 }
-
-#[derive(Serialize, Deserialize, Type, PartialEq, Debug)]
-pub struct BackendRequest {
-	pub cookie: String,
-	pub environment: HashMap<String, String>,
-	pub prompter_path: String,
-	pub identity: Identity,
-}
modifiedremowt/crates/remowt-link-shared/Cargo.tomldiffbeforeafterboth
--- a/remowt/crates/remowt-link-shared/Cargo.toml
+++ b/remowt/crates/remowt-link-shared/Cargo.toml
@@ -9,7 +9,7 @@
 anyhow.workspace = true
 bifrostlink.workspace = true
 bytes.workspace = true
-camino = { workspace = true }
+camino = { workspace = true, features = ["serde1"] }
 serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
 thiserror.workspace = true
addedremowt/crates/remowt-link-shared/src/gateway.rsdiffbeforeafterboth
--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/gateway.rs
@@ -0,0 +1,100 @@
+use std::path::{Path, PathBuf};
+
+use anyhow::{anyhow, Context as _};
+use bifrostlink::{Rpc, Rtt};
+use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
+use tokio::net::{UnixListener, UnixStream};
+use tracing::{debug, warn};
+use uuid::Uuid;
+
+use crate::port::child_port;
+use crate::{Address, BifConfig};
+
+pub const SOCKET_ENV: &str = "REMOWT_AGENT_SOCKET";
+
+pub const SOCKET_NAME: &str = "agent.sock";
+
+pub fn local_socket() -> anyhow::Result<PathBuf> {
+	let dir = std::env::var_os("XDG_RUNTIME_DIR").context("XDG_RUNTIME_DIR not set")?;
+	Ok(PathBuf::from(dir).join("remowt-local"))
+}
+
+pub fn socket_path() -> anyhow::Result<PathBuf> {
+	match std::env::var_os(SOCKET_ENV) {
+		Some(p) => Ok(PathBuf::from(p)),
+		None => local_socket(),
+	}
+}
+
+fn peer_tag(addr: &Address) -> u8 {
+	match addr {
+		Address::User => 0,
+		Address::Agent => 1,
+		Address::AgentPrivileged => 2,
+		_ => unreachable!(),
+	}
+}
+fn untag_peer(tag: u8) -> anyhow::Result<Address> {
+	Ok(match tag {
+		0 => Address::User,
+		1 => Address::Agent,
+		2 => Address::AgentPrivileged,
+		_ => unreachable!(),
+	})
+}
+
+pub async fn serve(rpc: Rpc<BifConfig>, path: &Path) -> anyhow::Result<()> {
+	let _ = tokio::fs::remove_file(path).await;
+	let listener = UnixListener::bind(path)
+		.with_context(|| format!("binding agent gateway at {}", path.display()))?;
+	let tag = peer_tag(&rpc.me());
+	tokio::spawn(async move {
+		loop {
+			let mut stream = match listener.accept().await {
+				Ok((stream, _)) => stream,
+				Err(e) => {
+					warn!("gateway accept failed: {e}");
+					continue;
+				}
+			};
+			let id = Uuid::new_v4().as_u128();
+			let mut hello = [0u8; 17];
+			hello[0] = tag;
+			hello[1..].copy_from_slice(&id.to_be_bytes());
+			if let Err(e) = stream.write_all(&hello).await {
+				warn!("gateway handshake failed: {e}");
+				continue;
+			}
+			debug!("gateway client {id:032x}");
+			let (rx, tx) = stream.into_split();
+			rpc.add_direct(
+				Address::Ephemeral(Uuid::from_u128(id)),
+				child_port(rx, tx),
+				Rtt(0),
+			);
+		}
+	});
+	Ok(())
+}
+
+pub async fn connect(path: &Path) -> anyhow::Result<Rpc<BifConfig>> {
+	let mut stream = UnixStream::connect(path)
+		.await
+		.with_context(|| format!("connecting to agent gateway at {}", path.display()))?;
+
+	let mut hello = [0u8; 17];
+	stream
+		.read_exact(&mut hello)
+		.await
+		.context("reading gateway handshake")?;
+	let peer = untag_peer(hello[0])?;
+	let id = u128::from_be_bytes(hello[1..].try_into().expect("16 bytes"));
+
+	let (rx, tx) = stream.into_split();
+	let rpc = Rpc::<BifConfig>::new(Address::Ephemeral(Uuid::from_u128(id)));
+	rpc.add_direct(peer, child_port(rx, tx), Rtt(0));
+	rpc.wait_for_connection_to(Address::User)
+		.await
+		.map_err(|_| anyhow!("no route to the User through the agent"))?;
+	Ok(rpc)
+}
modifiedremowt/crates/remowt-ui-prompt/Cargo.tomldiffbeforeafterboth
--- a/remowt/crates/remowt-ui-prompt/Cargo.toml
+++ b/remowt/crates/remowt-ui-prompt/Cargo.toml
@@ -2,19 +2,15 @@
 name = "remowt-ui-prompt"
 description = "Interactive UI prompt endpoint for remowt (D-Bus)"
 version.workspace = true
-edition = "2021"
+edition.workspace = true
 license.workspace = true
 
 [dependencies]
 anyhow.workspace = true
 bifrostlink.workspace = true
 bifrostlink-macros.workspace = true
+remowt-link-shared.workspace = true
 serde.workspace = true
 thiserror.workspace = true
 tokio = { workspace = true, features = ["io-util", "macros", "process", "rt"] }
 tracing.workspace = true
-zbus = { workspace = true, optional = true }
-
-[features]
-default = ["dbus"]
-dbus = ["dep:zbus"]
modifiedremowt/crates/remowt-ui-prompt/src/auto.rsdiffbeforeafterboth
--- a/remowt/crates/remowt-ui-prompt/src/auto.rs
+++ b/remowt/crates/remowt-ui-prompt/src/auto.rs
@@ -1,45 +1,41 @@
-use anyhow::bail;
+use std::path::Path;
+
+use bifrostlink::declarative::RemoteEndpoints as _;
+use remowt_link_shared::{Address, BifConfig, gateway};
 use tracing::debug;
-use zbus::fdo::DBusProxy;
-use zbus::names::BusName;
 
-use crate::dbus::{DbusPrompterProxy, BUS_NAME, PROMPTER_PATH};
+use crate::bifrost::PromptEndpointsClient;
 use crate::rofi::RofiPrompter;
 use crate::{Prompter, Result, Source};
 
 pub struct AutoPrompter {
-	dbus: Option<DbusPrompterProxy<'static>>,
+	remote: Option<PromptEndpointsClient<BifConfig>>,
 	fallback: RofiPrompter,
 }
 
 impl AutoPrompter {
 	pub async fn new() -> Self {
-		let dbus = match Self::try_dbus().await {
-			Ok(p) => Some(p),
+		let remote = match gateway::local_socket() {
+			Ok(path) => Self::try_connect(&path).await,
 			Err(e) => {
-				debug!("dbus prompter unavailable, falling back to rofi: {e}");
+				debug!("no local gateway socket, falling back to rofi: {e}");
 				None
 			}
 		};
 		Self {
-			dbus,
+			remote,
 			fallback: RofiPrompter,
 		}
 	}
 
-	async fn try_dbus() -> anyhow::Result<DbusPrompterProxy<'static>> {
-		let conn = zbus::Connection::session().await?;
-		let dbus = DBusProxy::new(&conn).await?;
-		let name = BusName::try_from(BUS_NAME)?;
-		if !dbus.name_has_owner(name).await? {
-			bail!("{BUS_NAME} not registered on session bus");
+	async fn try_connect(path: &Path) -> Option<PromptEndpointsClient<BifConfig>> {
+		match gateway::connect(path).await {
+			Ok(rpc) => Some(PromptEndpointsClient::wrap(rpc.remote(Address::User))),
+			Err(e) => {
+				debug!("local prompt agent unavailable, falling back to rofi: {e}");
+				None
+			}
 		}
-		let proxy = DbusPrompterProxy::builder(&conn)
-			.destination(BUS_NAME)?
-			.path(PROMPTER_PATH)?
-			.build()
-			.await?;
-		Ok(proxy)
 	}
 }
 
@@ -51,8 +47,8 @@
 		variants: &[&str],
 		source: &[Source],
 	) -> Result<u32> {
-		if let Some(dbus) = &self.dbus {
-			return Prompter::prompt_enum(dbus, prompt, description, variants, source).await;
+		if let Some(remote) = &self.remote {
+			return Prompter::prompt_enum(remote, prompt, description, variants, source).await;
 		}
 		self.fallback
 			.prompt_enum(prompt, description, variants, source)
@@ -66,8 +62,8 @@
 		description: &str,
 		source: &[Source],
 	) -> Result<String> {
-		if let Some(dbus) = &self.dbus {
-			return Prompter::prompt_text(dbus, echo, prompt, description, source).await;
+		if let Some(remote) = &self.remote {
+			return Prompter::prompt_text(remote, echo, prompt, description, source).await;
 		}
 		self.fallback
 			.prompt_text(echo, prompt, description, source)
@@ -75,8 +71,8 @@
 	}
 
 	async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
-		if let Some(dbus) = &self.dbus {
-			return Prompter::display_text(dbus, error, description, source).await;
+		if let Some(remote) = &self.remote {
+			return Prompter::display_text(remote, error, description, source).await;
 		}
 		self.fallback.display_text(error, description, source).await
 	}
deletedremowt/crates/remowt-ui-prompt/src/dbus.rsdiffbeforeafterboth
--- a/remowt/crates/remowt-ui-prompt/src/dbus.rs
+++ /dev/null
@@ -1,143 +0,0 @@
-use zbus::interface;
-use zbus::{fdo, proxy};
-
-use crate::Source;
-use crate::{BlockingPrompter, Result};
-use crate::{Error, Prompter};
-
-pub const BUS_NAME: &str = "lach.RemowtAskpass";
-pub const PROMPTER_PATH: &str = "/lach/Askpass";
-
-pub struct DbusPrompterInterface<P>(pub P);
-
-#[interface(name = "lach.PolkitInputHandler")]
-impl<P: Prompter + Send + Sync + 'static> DbusPrompterInterface<P> {
-	async fn prompt_enum(
-		&self,
-		prompt: &str,
-		description: &str,
-		variants: Vec<String>,
-		source: Vec<Source>,
-	) -> fdo::Result<u32> {
-		let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
-		Ok(self
-			.0
-			.prompt_enum(prompt, description, &variants, &source)
-			.await?)
-	}
-	async fn prompt_text(
-		&self,
-		echo: bool,
-		prompt: &str,
-		description: &str,
-		source: Vec<Source>,
-	) -> fdo::Result<String> {
-		Ok(self
-			.0
-			.prompt_text(echo, prompt, description, &source)
-			.await?)
-	}
-	async fn display_text(
-		&self,
-		error: bool,
-		description: &str,
-		source: Vec<Source>,
-	) -> fdo::Result<()> {
-		Ok(self.0.display_text(error, description, &source).await?)
-	}
-}
-
-#[proxy(interface = "lach.PolkitInputHandler")]
-pub trait DbusPrompter {
-	async fn prompt_enum(
-		&self,
-		prompt: &str,
-		description: &str,
-		variants: &[&str],
-		source: &[Source],
-	) -> fdo::Result<u32>;
-	async fn prompt_text(
-		&self,
-		echo: bool,
-		prompt: &str,
-		description: &str,
-		source: &[Source],
-	) -> fdo::Result<String>;
-	async fn display_text(
-		&self,
-		error: bool,
-		description: &str,
-		source: &[Source],
-	) -> fdo::Result<()>;
-}
-
-impl Prompter for DbusPrompterProxy<'_> {
-	async fn prompt_enum(
-		&self,
-		prompt: &str,
-		description: &str,
-		variants: &[&str],
-		source: &[Source],
-	) -> Result<u32> {
-		Ok(self
-			.prompt_enum(prompt, description, variants, source)
-			.await?)
-	}
-
-	async fn prompt_text(
-		&self,
-		echo: bool,
-		prompt: &str,
-		description: &str,
-		source: &[Source],
-	) -> Result<String> {
-		Ok(self.prompt_text(echo, prompt, description, source).await?)
-	}
-
-	async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
-		Ok(self.display_text(error, description, source).await?)
-	}
-}
-impl BlockingPrompter for DbusPrompterProxyBlocking<'_> {
-	fn prompt_enum(
-		&self,
-		prompt: &str,
-		description: &str,
-		variants: &[&str],
-		source: &[Source],
-	) -> Result<u32> {
-		Ok(self.prompt_enum(prompt, description, variants, source)?)
-	}
-
-	fn prompt_text(
-		&self,
-		echo: bool,
-		prompt: &str,
-		description: &str,
-		source: &[Source],
-	) -> Result<String> {
-		Ok(self.prompt_text(echo, prompt, description, source)?)
-	}
-
-	fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
-		Ok(self.display_text(error, description, source)?)
-	}
-}
-
-impl From<fdo::Error> for Error {
-	fn from(value: fdo::Error) -> Self {
-		if matches!(value, fdo::Error::NoReply(_)) {
-			return Self::Cancel;
-		}
-		Self::InputError(format!("{value}"))
-	}
-}
-impl From<Error> for fdo::Error {
-	fn from(value: Error) -> Self {
-		match value {
-			Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),
-			Error::Remote(e) => fdo::Error::NoReply(format!("remote error occured: {e}")),
-			Error::InputError(e) => fdo::Error::Failed(e),
-		}
-	}
-}
modifiedremowt/crates/remowt-ui-prompt/src/lib.rsdiffbeforeafterboth
--- a/remowt/crates/remowt-ui-prompt/src/lib.rs
+++ b/remowt/crates/remowt-ui-prompt/src/lib.rs
@@ -5,7 +5,6 @@
 
 pub mod auto;
 pub mod bifrost;
-pub mod dbus;
 pub mod rofi;
 
 #[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]
@@ -20,7 +19,6 @@
 
 pub type Result<T, E = Error> = result::Result<T, E>;
 
-#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]
 #[derive(serde::Serialize, serde::Deserialize, Clone)]
 pub struct Source(pub Cow<'static, str>);
 impl fmt::Display for Source {