git.delta.rocks / remowt / refs/commits / 960ee4bca68c

difftreelog

feat remowt-askpass, remowt-editor

qprrsuurYaroslav Bolyukin2026-01-25parent: #5f64fb1.patch.diff
in: trunk

5 files changed

modifiedcmds/remowt-agent/Cargo.tomldiffbeforeafterboth
--- a/cmds/remowt-agent/Cargo.toml
+++ b/cmds/remowt-agent/Cargo.toml
@@ -4,22 +4,32 @@
 edition = "2021"
 
 [dependencies]
-anyhow = "1.0.86"
+anyhow.workspace = true
 bifrostlink.workspace = true
 bifrostlink-ports.workspace = true
-clap = { version = "4.5.13", features = ["derive"] }
-futures = "0.3.30"
-futures-util = "0.3.30"
-nix = "0.29.0"
-polkit-shared = { version = "0.1.0", path = "../../crates/polkit-shared" }
-rand = "0.8.5"
-remowt-link-shared = { version = "0.1.0", path = "../../crates/remowt-link-shared" }
-serde = { version = "1.0.204", features = ["derive"] }
-tokio = { version = "1.39.2", features = ["rt-multi-thread", "fs", "macros"] }
-tokio-util = { version = "0.7.11", features = ["codec"] }
-tracing = "0.1.40"
-tracing-subscriber = "0.3.18"
-ui-prompt = { version = "0.1.0", path = "../../crates/ui-prompt" }
-uuid = { version = "1.10.0", features = ["v4"] }
-zbus = { version = "4.4.0", features = ["tokio"] }
-zbus_polkit = { version = "4.0.0", features = ["tokio"] }
+clap = { workspace = true, features = ["derive"] }
+futures.workspace = true
+futures-util.workspace = true
+nix.workspace = true
+polkit-shared.workspace = true
+rand.workspace = true
+remowt-link-shared.workspace = true
+remowt-pty.workspace = true
+serde = { workspace = true, features = ["derive"] }
+tempfile.workspace = true
+tokio = { workspace = true, features = [
+	"rt",
+	"fs",
+	"macros",
+	"net",
+	"io-util",
+	"time",
+	"process",
+] }
+tokio-util = { workspace = true, features = ["codec"] }
+tracing.workspace = true
+tracing-subscriber.workspace = true
+ui-prompt.workspace = true
+uuid = { workspace = true, features = ["v4"] }
+zbus = { workspace = true, features = ["tokio"] }
+zbus_polkit = { workspace = true, features = ["tokio"] }
addedcmds/remowt-agent/src/askpass.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/remowt-agent/src/askpass.rs
@@ -0,0 +1,50 @@
+use std::borrow::Cow;
+use std::io::Write as _;
+
+use anyhow::Context as _;
+use ui_prompt::bifrost::PromptEndpointsClient;
+use ui_prompt::dbus::{DbusPrompterInterface, DbusPrompterProxy};
+use ui_prompt::Source;
+use zbus::Connection;
+
+use remowt_link_shared::BifConfig;
+
+const BUS_NAME: &str = "lach.RemowtAskpass";
+const PROMPTER_PATH: &str = "/lach/Askpass";
+
+pub async fn serve(
+	conn: &Connection,
+	prompter: PromptEndpointsClient<BifConfig>,
+) -> anyhow::Result<()> {
+	conn.object_server()
+		.at(PROMPTER_PATH, DbusPrompterInterface(prompter))
+		.await?;
+	conn.request_name(BUS_NAME).await?;
+	Ok(())
+}
+
+pub async fn ask(prompt: &str, description: String) -> anyhow::Result<()> {
+	let conn = Connection::session()
+		.await
+		.context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;
+	let proxy = DbusPrompterProxy::builder(&conn)
+		.destination(BUS_NAME)?
+		.path(PROMPTER_PATH)?
+		.build()
+		.await?;
+
+	let password = proxy
+		.prompt_text(
+			false,
+			prompt,
+			&description,
+			&[Source(Cow::Borrowed("remowt-askpass"))],
+		)
+		.await?;
+
+	let mut out = std::io::stdout().lock();
+	out.write_all(password.as_bytes())?;
+	out.write_all(b"\n")?;
+	out.flush()?;
+	Ok(())
+}
addedcmds/remowt-agent/src/bus.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/remowt-agent/src/bus.rs
@@ -0,0 +1,40 @@
+use std::process::Stdio;
+
+use anyhow::Context as _;
+use futures::StreamExt as _;
+use tokio::process::{Child, Command};
+use tokio_util::codec::{FramedRead, LinesCodec};
+use zbus::Connection;
+
+pub struct PrivateBus {
+	pub address: String,
+	pub conn: Connection,
+	_child: Child,
+}
+
+pub async fn spawn() -> anyhow::Result<PrivateBus> {
+	let mut child = Command::new("dbus-daemon")
+		.args(["--session", "--nofork", "--print-address"])
+		.stdout(Stdio::piped())
+		.kill_on_drop(true)
+		.spawn()
+		.context("spawning dbus-daemon for the private bus")?;
+
+	let stdout = child.stdout.take().expect("piped");
+	let address = FramedRead::new(stdout, LinesCodec::new())
+		.next()
+		.await
+		.context("dbus-daemon exited before printing its address")?
+		.context("reading dbus-daemon address")?;
+
+	let conn = zbus::connection::Builder::address(address.as_str())?
+		.build()
+		.await
+		.context("connecting to the private bus")?;
+
+	Ok(PrivateBus {
+		address,
+		conn,
+		_child: child,
+	})
+}
addedcmds/remowt-agent/src/editor.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/remowt-agent/src/editor.rs
@@ -0,0 +1,119 @@
+use std::env::{current_dir, temp_dir};
+use std::path::Path;
+use std::time::Duration;
+use std::{fs, io};
+
+use anyhow::{bail, Context as _};
+use nix::libc;
+use remowt_link_shared::editor::EditorEndpointsClient;
+use tokio::process::Command;
+use zbus::{fdo, interface, proxy, Connection};
+
+use remowt_link_shared::BifConfig;
+
+const BUS_NAME: &str = "lach.RemowtEditor";
+const SERVICE_PATH: &str = "/lach/Editor";
+
+pub struct EditorService {
+	editor: EditorEndpointsClient<BifConfig>,
+}
+
+#[interface(name = "lach.RemowtEditor")]
+impl EditorService {
+	/// Attach the User's GUI to the nvim server at `socket_path` (on the remote),
+	/// blocking until the user is done.
+	async fn edit(&self, socket_path: String) -> fdo::Result<()> {
+		self.editor
+			.open_editor(socket_path)
+			.await
+			.map_err(|e| fdo::Error::Failed(format!("requesting editor on the User: {e}")))?
+			.map_err(|e| fdo::Error::Failed(format!("editor failed: {e}")))?;
+		Ok(())
+	}
+}
+
+pub async fn serve(
+	conn: &Connection,
+	editor: EditorEndpointsClient<BifConfig>,
+) -> anyhow::Result<()> {
+	conn.object_server()
+		.at(SERVICE_PATH, EditorService { editor })
+		.await?;
+	conn.request_name(BUS_NAME).await?;
+	Ok(())
+}
+
+#[proxy(interface = "lach.RemowtEditor")]
+trait RemowtEditor {
+	async fn edit(&self, socket_path: &str) -> fdo::Result<()>;
+}
+
+pub async fn edit(path: String) -> anyhow::Result<()> {
+	let path = Path::new(&path);
+	let abs = if path.is_absolute() {
+		path.to_path_buf()
+	} else {
+		current_dir()?.join(path)
+	};
+
+	let sock = temp_dir().join(format!("remowt-nvim-{}.sock", uuid::Uuid::new_v4()));
+	let sock_str = sock
+		.to_str()
+		.context("temp socket path is not utf-8")?
+		.to_owned();
+
+	let mut child = Command::new("nvim");
+	child
+		.arg("--headless")
+		.arg("--listen")
+		.arg(&sock)
+		.arg("--")
+		.arg(&abs)
+		.kill_on_drop(true);
+	// SAFETY: only an async-signal-safe `prctl` call.
+	unsafe {
+		child.pre_exec(|| {
+			if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL as libc::c_ulong) != 0 {
+				return Err(io::Error::last_os_error());
+			}
+			Ok(())
+		});
+	}
+	let mut child = child.spawn().context("spawning nvim")?;
+
+	wait_for_socket(&sock)
+		.await
+		.context("nvim did not start its server")?;
+
+	let conn = Connection::session()
+		.await
+		.context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;
+	let proxy = RemowtEditorProxy::builder(&conn)
+		.destination(BUS_NAME)?
+		.path(SERVICE_PATH)?
+		.build()
+		.await?;
+	let result = proxy.edit(&sock_str).await;
+
+	if tokio::time::timeout(Duration::from_secs(2), child.wait())
+		.await
+		.is_err()
+	{
+		let _ = child.kill().await;
+	}
+	let _ = fs::remove_file(&sock);
+
+	result?;
+	Ok(())
+}
+
+/// Poll for `path` to appear (nvim creating its listen socket), up to ~10s.
+async fn wait_for_socket(path: &Path) -> anyhow::Result<()> {
+	for _ in 0..200 {
+		if tokio::fs::try_exists(path).await.unwrap_or(false) {
+			return Ok(());
+		}
+		tokio::time::sleep(Duration::from_millis(50)).await;
+	}
+	bail!("timed out waiting for {}", path.display())
+}
modifiedcmds/remowt-agent/src/main.rsdiffbeforeafterboth
before · cmds/remowt-agent/src/main.rs
1use std::borrow::Cow;2use std::collections::{BTreeMap, HashMap};3use std::future;4use std::io::{stdout, Write};5use std::path::PathBuf;6use std::sync::{Arc, Mutex, OnceLock};78use bifrostlink::{AddressT, Rpc};9use bifrostlink_ports::unix_socket::from_socket;10use clap::Parser;11use polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};12use remowt_link_shared::Address;13use tokio::io::{AsyncReadExt, AsyncWriteExt};14use tokio::net::UnixStream;15use tokio::runtime::Runtime;16use tokio::task::AbortHandle;17use tracing::{info, trace};18use ui_prompt::rofi::RofiPrompter;19use ui_prompt::{PrependSourcePrompter, Prompter, Source};20use zbus::fdo;21use zbus::zvariant::{OwnedValue, Str};22use zbus::{interface, proxy, Connection};23use zbus_polkit::policykit1::Subject;2425use self::helper::{Helper, SuidHelper};2627pub mod helper;2829struct CancelTaskOnDrop {30	tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,31	handle: String,32}33impl Drop for CancelTaskOnDrop {34	fn drop(&mut self) {35		info!("cancel on drop");36		if let Some(task) = self37			.tasks38			.lock()39			.expect("not poisoned")40			.remove(&self.handle)41		{42			task.abort();43		}44	}45}4647struct Agent<H> {48	tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,49	helper: H,50}51impl<H> Agent<H> {52	fn new(helper: H) -> Self {53		Agent {54			tasks: Arc::new(Mutex::new(HashMap::new())),55			helper,56		}57	}58}5960#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]61impl<H> Agent<H>62where63	H: Helper + Clone + Send + Sync + 'static,64{65	/// BeginAuthentication method66	#[allow(clippy::too_many_arguments)]67	async fn begin_authentication(68		&self,69		action_id: String,70		message: String,71		icon_name: String,72		mut details: BTreeMap<String, String>,73		cookie: String,74		identities: Vec<Identity>,75	) -> zbus::fdo::Result<()> {76		use std::fmt::Write;77		info!("begin auth");78		let _cancel_guard = Arc::new(OnceLock::new());79		let task = {80			let helper = self.helper.clone();81			let cookie = cookie.clone();82			let _cancel_guard = _cancel_guard.clone();83			tokio::task::spawn(async move {84				let _cancel_guard = _cancel_guard.clone();85				trace!("conversation task");86				let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);87				if let Some(subject) = details.remove("polkit.caller-pid") {88					let _ = write!(description, "\n<b>Caller:</b> ");89					if let Ok(pid) = subject.parse::<u32>() {90						let _ = write!(description, "{}", PidDisplay(pid));91					} else {92						let _ = write!(description, "{}", emphasize("invalid pid"));93					}94				}95				if let Some(subject) = details.remove("polkit.subject-pid") {96					let _ = write!(description, "\n<b>Subject:</b> ");97					if let Ok(pid) = subject.parse::<u32>() {98						let _ = write!(description, "{}", PidDisplay(pid));99					} else {100						let _ = write!(description, "{}", emphasize("invalid pid"));101					}102				}103				let mut prompter = PrependSourcePrompter {104					source: vec![Source(Cow::Borrowed("polkit agent"))],105					description: description.clone(),106					prompter: RofiPrompter,107				};108109				let identity_displays: Vec<String> =110					identities.iter().map(|v| v.to_string()).collect();111				let identity_displays: Vec<&str> =112					identity_displays.iter().map(|v| v.as_str()).collect();113				info!("choose identity");114				let choosen_identity = match identity_displays.len() {115					0 => {116						return Err(fdo::Error::AuthFailed(117							"no identity to authenticate as".to_owned(),118						))119					}120					1 => 0,121					_ => {122						prompter123							.prompt_enum(124								"Identity",125								"Select identity to use for polkit authorization",126								&identity_displays,127								&[],128							)129							.await?130					}131				};132				info!("identity chosen");133134				let _ = write!(135					description,136					"\n<b>Identity:</b> {}",137					identities[choosen_identity as usize]138				);139				prompter.description = description;140141				prompter.source.push(Source(Cow::Borrowed("polkit daemon")));142143				helper144					.help_me(145						&cookie,146						prompter,147						identities[choosen_identity as usize].clone(),148					)149					.await150					.map_err(|e| fdo::Error::Failed(e.to_string()))?;151				// let connection = Connection::system().await?;152				// let helper = PolkitHelperProxy::new(&connection).await?;153154				Ok(())155			})156		};157		self.tasks158			.lock()159			.unwrap()160			.insert(cookie.clone(), task.abort_handle());161		info!("abort handle stored");162		let _ = _cancel_guard.set(CancelTaskOnDrop {163			tasks: self.tasks.clone(),164			handle: cookie.clone(),165		});166167		let _ = task.await;168169		Ok(())170	}171172	/// CancelAuthentication method173	async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {174		info!("auth cancelled");175		if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {176			info!("abort handle found");177			abort.abort();178		}179		// debug!("Authentication cancled ! {cookie}");180		Ok(())181	}182}183184const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";185186#[proxy(187	interface = "lach.PolkitHelper",188	default_service = "lach.polkit.helper1",189	default_path = "/lach/PolkitHelper"190)]191trait PolkitHelper {192	fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;193}194195#[derive(Parser)]196enum Opts {197	Agent,198	AskPass {199		description: String,200	},201	RealAgent {202		#[arg(long)]203		path: PathBuf,204	},205}206207fn main() -> anyhow::Result<()> {208	tracing_subscriber::fmt::init();209	let opts = Opts::parse();210211	let runtime = Runtime::new()?;212213	match opts {214		Opts::Agent => {215			// TODO: Setup env, directories with various things...216			runtime.block_on(main_agent())217		}218		Opts::AskPass { description } => runtime.block_on(main_askpass(description)),219		Opts::RealAgent { path } => runtime.block_on(main_real_agent(path)),220	}221}222async fn main_real_agent(path: PathBuf) -> anyhow::Result<()> {223    let mut stream = UnixStream::connect(path).await?;224    stream.write_all(b"REMOWT_HELLO\0").await?;225    let mut buf = [0u8; 12];226    stream.read_exact(&mut buf).await?;227    assert_eq!(&buf, b"REMOWT_EHLO\0");228    let port = from_socket(stream);229    let rpc = Rpc::<Address, remowt_link_shared::Error>::new(Address::Agent);230    rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));231    Ok(())232}233234async fn main_agent() -> anyhow::Result<()> {235	trace!("started");236	let conn = Connection::system().await?;237238	let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&conn).await?;239	conn.object_server()240		.at(OBJ_PATH, Agent::new(SuidHelper))241		.await?;242243	let session_id = std::env::var("XDG_SESSION_ID")?;244	let mut details = HashMap::new();245	let val: OwnedValue = {246		let wrapped: Str<'_> = session_id.into();247		wrapped.into()248	};249	details.insert("session-id".to_string(), val);250	proxy251		.register_authentication_agent(252			&Subject {253				subject_kind: "unix-session".to_string(),254				subject_details: details,255			},256			"C",257			OBJ_PATH,258		)259		.await?;260	future::pending().await261}262263async fn main_askpass(description: String) -> anyhow::Result<()> {264	let password = RofiPrompter265		.prompt_text(false, &description, "SSH password request", &[])266		.await?;267	stdout().lock().write_all(password.as_bytes())?;268	future::pending().await269}
after · 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::os::unix::fs::PermissionsExt as _;6use std::path::PathBuf;7use std::sync::{Arc, Mutex, OnceLock};89use bifrostlink::declarative::RemoteEndpoints;10use bifrostlink::Rpc;11use bifrostlink_ports::stdio::from_stdio;12use bifrostlink_ports::unix_socket::from_socket;13use clap::Parser;14use polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};15use remowt_link_shared::editor::EditorEndpointsClient;16use remowt_link_shared::{Address, BifConfig, Fs, Pty, Systemd};17use tokio::fs;18use tokio::net::UnixStream;19use tokio::runtime::Builder;20use tokio::task::AbortHandle;21use tracing::{info, trace};22use ui_prompt::bifrost::PromptEndpointsClient;23use ui_prompt::{PrependSourcePrompter, Prompter, Source};24use zbus::fdo;25use zbus::zvariant::{OwnedValue, Str};26use zbus::{interface, proxy, Connection};27use zbus_polkit::policykit1::Subject;2829use self::helper::{Helper, SocketHelper, SuidHelper};3031pub mod askpass;32pub mod bus;33pub mod editor;34pub mod helper;3536struct CancelTaskOnDrop {37	tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,38	handle: String,39}40impl Drop for CancelTaskOnDrop {41	fn drop(&mut self) {42		info!("cancel on drop");43		if let Some(task) = self44			.tasks45			.lock()46			.expect("not poisoned")47			.remove(&self.handle)48		{49			task.abort();50		}51	}52}5354struct Agent<H, P> {55	tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,56	helper: H,57	prompter: P,58}59impl<H, P> Agent<H, P> {60	fn new(helper: H, prompter: P) -> Self {61		Agent {62			tasks: Arc::new(Mutex::new(HashMap::new())),63			helper,64			prompter,65		}66	}67}6869#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]70impl<H, P> Agent<H, P>71where72	H: Helper + Clone + Send + Sync + 'static,73	P: Prompter + Clone + Send + Sync + 'static,74{75	/// BeginAuthentication method76	#[allow(clippy::too_many_arguments)]77	async fn begin_authentication(78		&self,79		action_id: String,80		message: String,81		_icon_name: String,82		mut details: BTreeMap<String, String>,83		cookie: String,84		identities: Vec<Identity>,85	) -> zbus::fdo::Result<()> {86		use std::fmt::Write;87		info!("begin auth");88		let _cancel_guard = Arc::new(OnceLock::new());89		let task = {90			let helper = self.helper.clone();91			let prompter = self.prompter.clone();92			let cookie = cookie.clone();93			let _cancel_guard = _cancel_guard.clone();94			tokio::task::spawn(async move {95				let _cancel_guard = _cancel_guard.clone();96				trace!("conversation task");97				let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);98				if let Some(subject) = details.remove("polkit.caller-pid") {99					let _ = write!(description, "\n<b>Caller:</b> ");100					if let Ok(pid) = subject.parse::<u32>() {101						let _ = write!(description, "{}", PidDisplay(pid));102					} else {103						let _ = write!(description, "{}", emphasize("invalid pid"));104					}105				}106				if let Some(subject) = details.remove("polkit.subject-pid") {107					let _ = write!(description, "\n<b>Subject:</b> ");108					if let Ok(pid) = subject.parse::<u32>() {109						let _ = write!(description, "{}", PidDisplay(pid));110					} else {111						let _ = write!(description, "{}", emphasize("invalid pid"));112					}113				}114				let mut prompter = PrependSourcePrompter {115					source: vec![Source(Cow::Borrowed("polkit agent"))],116					description: description.clone(),117					prompter,118				};119120				let identity_displays: Vec<String> =121					identities.iter().map(|v| v.to_string()).collect();122				let identity_displays: Vec<&str> =123					identity_displays.iter().map(|v| v.as_str()).collect();124				info!("choose identity");125				let choosen_identity = match identity_displays.len() {126					0 => {127						return Err(fdo::Error::AuthFailed(128							"no identity to authenticate as".to_owned(),129						))130					}131					1 => 0,132					_ => {133						prompter134							.prompt_enum(135								"Identity",136								"Select identity to use for polkit authorization",137								&identity_displays,138								&[],139							)140							.await?141					}142				};143				info!("identity chosen");144145				let _ = write!(146					description,147					"\n<b>Identity:</b> {}",148					identities[choosen_identity as usize]149				);150				prompter.description = description;151152				prompter.source.push(Source(Cow::Borrowed("polkit daemon")));153154				helper155					.help_me(156						&cookie,157						prompter,158						identities[choosen_identity as usize].clone(),159					)160					.await161					.map_err(|e| fdo::Error::Failed(e.to_string()))?;162				// let connection = Connection::system().await?;163				// let helper = PolkitHelperProxy::new(&connection).await?;164165				Ok(())166			})167		};168		self.tasks169			.lock()170			.unwrap()171			.insert(cookie.clone(), task.abort_handle());172		info!("abort handle stored");173		let _ = _cancel_guard.set(CancelTaskOnDrop {174			tasks: self.tasks.clone(),175			handle: cookie.clone(),176		});177178		let _ = task.await;179180		Ok(())181	}182183	/// CancelAuthentication method184	async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {185		info!("auth cancelled");186		if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {187			info!("abort handle found");188			abort.abort();189		}190		// debug!("Authentication cancled ! {cookie}");191		Ok(())192	}193}194195const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";196197#[proxy(198	interface = "lach.PolkitHelper",199	default_service = "lach.polkit.helper1",200	default_path = "/lach/PolkitHelper"201)]202trait PolkitHelper {203	fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;204}205206#[derive(Parser)]207enum Opts {208	AskPass {209		prompt: String,210		description: String,211	},212	Editor {213		/// Argument to nvim214		path: String,215	},216	RealAgent {217		#[arg(long)]218		path: Option<PathBuf>,219		/// Expect own address to be AgentPrivileged, skip installing polkit agent220		#[arg(long)]221		privileged: bool,222	},223}224225fn main() -> anyhow::Result<()> {226	// Log to stderr: `privileged-agent` uses stdout as the bifrost transport,227	// so anything written there would corrupt the stream.228	tracing_subscriber::fmt()229		.with_writer(std::io::stderr)230		.init();231	let opts = Opts::parse();232233	let runtime = Builder::new_current_thread().enable_all().build()?;234235	match opts {236		Opts::AskPass {237			prompt,238			description,239		} => runtime.block_on(askpass::ask(&prompt, description)),240		Opts::Editor { path } => runtime.block_on(editor::edit(path)),241		Opts::RealAgent { path, privileged } => runtime.block_on(main_real_agent(path, privileged)),242	}243}244async fn main_real_agent(path: Option<PathBuf>, privileged: bool) -> anyhow::Result<()> {245	let address = if privileged {246		Address::AgentPrivileged247	} else {248		Address::Agent249	};250	let mut rpc = Rpc::<BifConfig>::new(address);251252	Fs::new().register_endpoints(&mut rpc);253	Systemd.register_endpoints(&mut rpc);254	Pty::new().register_endpoints(&mut rpc);255256	let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));257	let editor_client = EditorEndpointsClient::wrap(rpc.remote(Address::User));258259	let bus = bus::spawn().await?;260	askpass::serve(&bus.conn, user_prompter.clone()).await?;261	editor::serve(&bus.conn, editor_client).await?;262263	let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;264	let exe = std::env::current_exe()?;265	let askpass_helper = helpers.path().join("remowt-askpass");266	let editor_helper = helpers.path().join("remowt-editor");267	{268		let script = format!(269			"#!/bin/sh\nexec {} ask-pass \"password\" \"$1\"\n",270			sh_quote(&exe.to_string_lossy())271		);272		fs::write(&askpass_helper, script).await?;273		fs::set_permissions(&askpass_helper, Permissions::from_mode(0o755)).await?;274	}275	{276		let script = format!(277			"#!/bin/sh\nexec {} editor \"$1\"\n",278			sh_quote(&exe.to_string_lossy())279		);280		fs::write(&editor_helper, script).await?;281		fs::set_permissions(&editor_helper, Permissions::from_mode(0o755)).await?;282	}283284	// Safety: Hoping tokio own threads won't read any of those...285	unsafe {286		prepend_path(helpers.path());287		std::env::set_var("SUDO_ASKPASS", &askpass_helper);288		std::env::set_var("SSH_ASKPASS", &askpass_helper);289		std::env::set_var("SSH_ASKPASS_REQUIRE", "force");290		std::env::set_var("EDITOR", &editor_helper);291		std::env::set_var("VISUAL", &editor_helper);292		std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &bus.address);293	}294295	let port = match path {296		Some(path) => from_socket(UnixStream::connect(path).await?),297		None => from_stdio(),298	};299	rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));300301	let polkit_conn = if !privileged {302		// The unprivileged agent doubles as a polkit authentication agent so303		// `run0` (e.g. our own elevation) routes its prompt to the User over304		// bifrost instead of failing on a tty-less session.305		let conn = Connection::system().await?;306		let helper = SocketHelper {307			fallback: SuidHelper,308		};309		register_auth_agent(&conn, Agent::new(helper, user_prompter)).await?;310		Some(conn)311	} else {312		None313	};314315	let _keep_alive = (bus, helpers, polkit_conn);316	pending().await317}318319async fn register_auth_agent<H, P>(conn: &Connection, agent: Agent<H, P>) -> anyhow::Result<()>320where321	H: Helper + Clone + Send + Sync + 'static,322	P: Prompter + Clone + Send + Sync + 'static,323{324	let proxy = zbus_polkit::policykit1::AuthorityProxy::new(conn).await?;325	conn.object_server().at(OBJ_PATH, agent).await?;326327	let subject = auth_agent_subject()?;328	proxy329		.register_authentication_agent(&subject, "C", OBJ_PATH)330		.await?;331	info!(kind = subject.subject_kind, "registered polkit agent");332	Ok(())333}334335fn auth_agent_subject() -> anyhow::Result<Subject> {336	let mut details = HashMap::new();337	if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {338		let val: OwnedValue = Str::from(session_id).into();339		details.insert("session-id".to_string(), val);340		return Ok(Subject {341			subject_kind: "unix-session".to_string(),342			subject_details: details,343		});344	}345346	details.insert("pid".to_string(), OwnedValue::from(std::process::id()));347	Ok(Subject {348		subject_kind: "unix-process".to_string(),349		subject_details: details,350	})351}352353fn sh_quote(s: &str) -> String {354	format!("'{}'", s.replace('\'', "'\\''"))355}356357/// Prepend `dir` to the process `PATH`.358///359/// # SAFETY360///361/// Same as `set_var`362unsafe fn prepend_path(dir: &std::path::Path) {363	let value = match std::env::var_os("PATH") {364		Some(existing) => {365			let mut v = dir.as_os_str().to_owned();366			v.push(":");367			v.push(existing);368			v369		}370		None => dir.as_os_str().to_owned(),371	};372	unsafe {373		std::env::set_var("PATH", value);374	}375}