Skip to repository content571 lines · 21.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:01:49.542Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
askpass.rs
1mod encrypted_password;
2
3pub use encrypted_password::{EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
4
5use net::async_net::UnixListener;
6use smol::lock::Mutex;
7#[cfg(not(target_os = "windows"))]
8use util::fs::make_file_executable;
9
10use std::ffi::OsStr;
11use std::ops::ControlFlow;
12use std::sync::Arc;
13use std::sync::OnceLock;
14use std::time::Duration;
15
16use anyhow::{Context as _, Result};
17use futures::channel::{mpsc, oneshot};
18use futures::{
19 AsyncBufReadExt as _, AsyncWriteExt as _, FutureExt as _, SinkExt, StreamExt, io::BufReader,
20 select_biased,
21};
22use gpui::{AsyncApp, BackgroundExecutor, Task};
23#[cfg(not(target_os = "windows"))]
24use smol::fs;
25use util::{ResultExt as _, debug_panic, maybe};
26
27#[cfg(not(target_os = "windows"))]
28use util::{paths::PathExt, shell::ShellKind};
29
30/// Path to the program used for askpass
31///
32/// On Unix and remote servers, this defaults to the current executable.
33/// On Windows, this must be set to the CLI variant of zed via set_askpass_program(),
34/// because SSH_ASKPASS must point to a directly executable binary. The CLI binary
35/// handles the ZED_ASKPASS_SOCKET env var to communicate with Zed over a Unix socket
36/// without needing a wrapper script.
37static ASKPASS_PROGRAM: OnceLock<std::path::PathBuf> = OnceLock::new();
38
39#[derive(PartialEq, Eq)]
40pub enum AskPassResult {
41 CancelledByUser,
42 Timedout,
43}
44
45pub struct AskPassDelegate {
46 tx: mpsc::UnboundedSender<(String, oneshot::Sender<EncryptedPassword>)>,
47 executor: BackgroundExecutor,
48 _task: Task<()>,
49}
50
51impl AskPassDelegate {
52 pub fn new(
53 cx: &mut AsyncApp,
54 password_prompt: impl Fn(String, oneshot::Sender<EncryptedPassword>, &mut AsyncApp)
55 + Send
56 + Sync
57 + 'static,
58 ) -> Self {
59 let (tx, mut rx) = mpsc::unbounded::<(String, oneshot::Sender<_>)>();
60 let task = cx.spawn(async move |cx: &mut AsyncApp| {
61 while let Some((prompt, channel)) = rx.next().await {
62 password_prompt(prompt, channel, cx);
63 }
64 });
65 Self {
66 tx,
67 _task: task,
68 executor: cx.background_executor().clone(),
69 }
70 }
71
72 pub fn ask_password(&mut self, prompt: String) -> Task<Option<EncryptedPassword>> {
73 let mut this_tx = self.tx.clone();
74 self.executor.spawn(async move {
75 let (tx, rx) = oneshot::channel();
76 this_tx.send((prompt, tx)).await.ok()?;
77 rx.await.ok()
78 })
79 }
80}
81
82pub struct AskPassSession {
83 #[cfg(target_os = "windows")]
84 secret: std::sync::Arc<std::sync::Mutex<Option<EncryptedPassword>>>,
85 askpass_task: PasswordProxy,
86 askpass_opened_rx: Option<oneshot::Receiver<()>>,
87 askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
88 executor: BackgroundExecutor,
89}
90
91#[cfg(not(target_os = "windows"))]
92const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
93
94#[cfg(not(target_os = "windows"))]
95const GPG_WRAPPER_SCRIPT_NAME: &str = "gpg-wrapper.sh";
96
97impl AskPassSession {
98 /// This will create a new AskPassSession.
99 /// You must retain this session until the master process exits.
100 #[must_use]
101 pub async fn new(executor: BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
102 #[cfg(target_os = "windows")]
103 let secret = std::sync::Arc::new(std::sync::Mutex::new(None));
104
105 let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
106
107 let askpass_opened_tx = Arc::new(Mutex::new(Some(askpass_opened_tx)));
108
109 let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
110 let kill_tx = Arc::new(Mutex::new(Some(askpass_kill_master_tx)));
111
112 let get_password = {
113 let executor = executor.clone();
114
115 #[cfg(target_os = "windows")]
116 let askpass_secret = secret.clone();
117 move |prompt| {
118 let prompt = delegate.ask_password(prompt);
119 let kill_tx = kill_tx.clone();
120 let askpass_opened_tx = askpass_opened_tx.clone();
121 #[cfg(target_os = "windows")]
122 let askpass_secret = askpass_secret.clone();
123 executor.spawn(async move {
124 if let Some(askpass_opened_tx) = askpass_opened_tx.lock().await.take() {
125 askpass_opened_tx.send(()).ok();
126 }
127 if let Some(password) = prompt.await {
128 #[cfg(target_os = "windows")]
129 {
130 askpass_secret.lock().unwrap().replace(password.clone());
131 }
132 ControlFlow::Continue(Ok(password))
133 } else {
134 if let Some(kill_tx) = kill_tx.lock().await.take() {
135 kill_tx.send(()).ok();
136 }
137 ControlFlow::Break(())
138 }
139 })
140 }
141 };
142 let askpass_task = PasswordProxy::new(Box::new(get_password), executor.clone()).await?;
143
144 Ok(Self {
145 #[cfg(target_os = "windows")]
146 secret,
147
148 askpass_task,
149 askpass_kill_master_rx: Some(askpass_kill_master_rx),
150 askpass_opened_rx: Some(askpass_opened_rx),
151 executor,
152 })
153 }
154
155 // This will run the askpass task forever, resolving as many authentication requests as needed.
156 // The caller is responsible for examining the result of their own commands and cancelling this
157 // future when this is no longer needed. Note that this can only be called once, but due to the
158 // drop order this takes an &mut, so you can `drop()` it after you're done with the master process.
159 //
160 // When `timeout` is provided, this resolves with `AskPassResult::Timedout` if no askpass prompt
161 // has been opened within that duration. This is intended for connection establishment (e.g.
162 // SSH), where "no prompt and no connection" indicates an unreachable host. Callers wrapping
163 // commands that may legitimately run for a long time without prompting (e.g. git) should pass
164 // `None` and rely on the command's own completion instead.
165 pub async fn run(&mut self, timeout: Option<Duration>) -> AskPassResult {
166 let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once");
167 let askpass_kill_master_rx = self
168 .askpass_kill_master_rx
169 .take()
170 .expect("Only call run once");
171 let executor = self.executor.clone();
172 let timer = async move {
173 match timeout {
174 Some(timeout) => executor.timer(timeout).await,
175 None => std::future::pending().await,
176 }
177 };
178
179 select_biased! {
180 _ = askpass_opened_rx.fuse() => {
181 // Note: this await can only resolve after we are dropped.
182 askpass_kill_master_rx.await.ok();
183 AskPassResult::CancelledByUser
184 }
185
186 _ = futures::FutureExt::fuse(timer) => {
187 AskPassResult::Timedout
188 }
189 }
190 }
191
192 /// This will return the password that was last set by the askpass script.
193 #[cfg(target_os = "windows")]
194 pub fn get_password(&self) -> Option<EncryptedPassword> {
195 self.secret.lock().ok()?.clone()
196 }
197
198 /// Returns the value to set as SSH_ASKPASS.
199 /// On Unix this is the path to the generated shell script.
200 /// On Windows this is the path to cli.exe directly — no script needed.
201 pub fn script_path(&self) -> impl AsRef<OsStr> {
202 self.askpass_task.script_path()
203 }
204
205 /// Path to a script suitable for git's `gpg.program`, routing GnuPG
206 /// passphrase prompts through Zed's askpass UI. `None` if unavailable.
207 pub fn gpg_wrapper_path(&self) -> Option<&std::path::Path> {
208 #[cfg(not(target_os = "windows"))]
209 return self.askpass_task.gpg_wrapper_path();
210 #[cfg(target_os = "windows")]
211 return None;
212 }
213
214 /// Returns the socket path to set as ZED_ASKPASS_SOCKET.
215 ///
216 /// On Windows, SSH_ASKPASS points directly to cli.exe. SSH passes only
217 /// the prompt string as argv[1] with no mechanism for extra arguments,
218 /// so the socket path is communicated via this environment variable instead.
219 /// cli.exe must check ZED_ASKPASS_SOCKET before clap parses args.
220 #[cfg(target_os = "windows")]
221 pub fn socket_path(&self) -> impl AsRef<OsStr> {
222 self.askpass_task.socket_path()
223 }
224}
225
226pub struct PasswordProxy {
227 _task: Task<()>,
228 /// On Unix: path to the generated .sh askpass script (set as SSH_ASKPASS).
229 /// On Windows: path to cli.exe (set as SSH_ASKPASS directly — no script needed).
230 askpass_script_path: std::path::PathBuf,
231 #[cfg(not(target_os = "windows"))]
232 gpg_wrapper_script_path: Option<std::path::PathBuf>,
233 /// On Windows only: path to the Unix socket, passed as ZED_ASKPASS_SOCKET
234 /// so cli.exe can find it without --askpass argument parsing.
235 #[cfg(target_os = "windows")]
236 askpass_socket_path: std::path::PathBuf,
237}
238
239impl PasswordProxy {
240 pub async fn new(
241 mut get_password: Box<
242 dyn FnMut(String) -> Task<ControlFlow<(), Result<EncryptedPassword>>>
243 + 'static
244 + Send
245 + Sync,
246 >,
247 executor: BackgroundExecutor,
248 ) -> Result<Self> {
249 let temp_dir = tempfile::Builder::new().prefix("omega-askpass").tempdir()?;
250 let askpass_socket = temp_dir.path().join("askpass.sock");
251 let current_exec =
252 std::env::current_exe().context("Failed to determine current Omega executable path.")?;
253
254 let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec);
255
256 // Unix: SSH_ASKPASS = path to generated .sh script in temp dir.
257 // Windows: SSH_ASKPASS = path to cli.exe directly. No script is written.
258 #[cfg(not(target_os = "windows"))]
259 let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
260 #[cfg(target_os = "windows")]
261 let askpass_script_path = askpass_program.to_path_buf();
262
263 let askpass_socket_path = askpass_socket.clone();
264
265 // Create a gpg wrapper script that routes GnuPG passphrase prompts through
266 // the same socket (and thus through Zed's askpass UI). This only works on
267 // Unix where we control the pinentry via loopback mode. We compute the path
268 // before the socket task takes ownership of `temp_dir`, and write the file
269 // afterwards.
270 #[cfg(not(target_os = "windows"))]
271 let (gpg_wrapper_script_path, gpg_wrapper_script) =
272 match generate_gpg_wrapper_script(askpass_program, &askpass_socket_path) {
273 Ok(script) => (
274 Some(temp_dir.path().join(GPG_WRAPPER_SCRIPT_NAME)),
275 Some(script),
276 ),
277 Err(err) => {
278 log::warn!("could not create gpg askpass wrapper: {err:#}");
279 (None, None)
280 }
281 };
282
283 let _task = executor.spawn(async move {
284 maybe!(async move {
285 let listener =
286 UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
287
288 while let Ok((mut stream, _)) = listener.accept().await {
289 let mut buffer = Vec::new();
290 let mut reader = BufReader::new(&mut stream);
291 if reader.read_until(b'\0', &mut buffer).await.is_err() {
292 buffer.clear();
293 }
294 let prompt = String::from_utf8_lossy(&buffer).into_owned();
295 let password = get_password(prompt).await;
296 match password {
297 ControlFlow::Continue(password) => {
298 if let Ok(password) = password
299 && let Ok(decrypted) =
300 password.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)
301 {
302 stream.write_all(decrypted.as_bytes()).await.log_err();
303 }
304 }
305 ControlFlow::Break(()) => {
306 // note: we expect the caller to drop this task when it's done.
307 // We need to keep the stream open until the caller is done to avoid
308 // spurious errors from ssh.
309 std::future::pending::<()>().await;
310 drop(stream);
311 }
312 }
313 }
314 drop(temp_dir);
315 Result::<_, anyhow::Error>::Ok(())
316 })
317 .await
318 .log_err();
319 });
320
321 // Unix only: write the shell script and mark it executable.
322 // On Windows cli.exe is invoked directly, so no script is needed.
323 #[cfg(not(target_os = "windows"))]
324 {
325 let askpass_script = generate_askpass_script(askpass_program, &askpass_socket_path)?;
326 fs::write(&askpass_script_path, askpass_script)
327 .await
328 .with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
329 make_file_executable(&askpass_script_path)
330 .await
331 .with_context(|| {
332 format!("marking askpass script executable at {askpass_script_path:?}")
333 })?;
334 }
335
336 // Write the gpg wrapper script (computed above) and mark it executable.
337 #[cfg(not(target_os = "windows"))]
338 let gpg_wrapper_script_path =
339 if let Some((path, script)) = gpg_wrapper_script_path.zip(gpg_wrapper_script) {
340 match async {
341 fs::write(&path, script)
342 .await
343 .with_context(|| format!("creating gpg wrapper script at {path:?}"))?;
344 make_file_executable(&path).await.with_context(|| {
345 format!("marking gpg wrapper script executable at {path:?}")
346 })?;
347 anyhow::Ok(())
348 }
349 .await
350 {
351 Ok(()) => Some(path),
352 Err(err) => {
353 log::warn!("could not write gpg askpass wrapper: {err:#}");
354 None
355 }
356 }
357 } else {
358 None
359 };
360
361 Ok(Self {
362 _task,
363 askpass_script_path,
364 #[cfg(not(target_os = "windows"))]
365 gpg_wrapper_script_path,
366 #[cfg(target_os = "windows")]
367 askpass_socket_path,
368 })
369 }
370
371 pub fn script_path(&self) -> impl AsRef<OsStr> {
372 &self.askpass_script_path
373 }
374
375 #[cfg(target_os = "windows")]
376 pub fn socket_path(&self) -> impl AsRef<OsStr> {
377 &self.askpass_socket_path
378 }
379
380 #[cfg(not(target_os = "windows"))]
381 pub fn gpg_wrapper_path(&self) -> Option<&std::path::Path> {
382 self.gpg_wrapper_script_path.as_deref()
383 }
384}
385
386/// Runs Zed in netcat mode for use in askpass.
387pub fn main(socket: &str) {
388 use std::io::{self, Read};
389 use std::process::exit;
390
391 let mut buffer = Vec::new();
392 if let Err(err) = io::stdin().read_to_end(&mut buffer) {
393 eprintln!("Error reading from stdin: {}", err);
394 exit(1);
395 }
396
397 connect_and_write_prompt(socket, buffer)
398}
399
400/// Runs Zed in askpass mode using prompts passed as arguments.
401pub fn main_from_args(socket: &str, args: impl IntoIterator<Item = String>) {
402 let prompt = args.into_iter().collect::<Vec<_>>().join("\0");
403 connect_and_write_prompt(socket, prompt.into_bytes())
404}
405
406fn connect_and_write_prompt(socket: &str, mut buffer: Vec<u8>) {
407 use net::UnixStream;
408 use std::io::{self, Read, Write};
409 use std::process::exit;
410
411 let mut stream = match UnixStream::connect(socket) {
412 Ok(stream) => stream,
413 Err(err) => {
414 eprintln!("Error connecting to socket {}: {}", socket, err);
415 exit(1);
416 }
417 };
418
419 #[cfg(target_os = "windows")]
420 while buffer.last().is_some_and(|&b| b == b'\n' || b == b'\r') {
421 buffer.pop();
422 }
423 if buffer.last() != Some(&b'\0') {
424 buffer.push(b'\0');
425 }
426
427 if let Err(err) = stream.write_all(&buffer) {
428 eprintln!("Error writing to socket: {}", err);
429 exit(1);
430 }
431
432 let mut response = Vec::new();
433 if let Err(err) = stream.read_to_end(&mut response) {
434 eprintln!("Error reading from socket: {}", err);
435 exit(1);
436 }
437
438 if let Err(err) = io::stdout().write_all(&response) {
439 eprintln!("Error writing to stdout: {}", err);
440 exit(1);
441 }
442}
443
444pub fn set_askpass_program(path: std::path::PathBuf) {
445 if ASKPASS_PROGRAM.set(path).is_err() {
446 debug_panic!("askpass program has already been set");
447 }
448}
449
450/// Generates the Unix shell askpass script.
451/// Not used on Windows — cli.exe is invoked directly as SSH_ASKPASS.
452#[cfg(not(target_os = "windows"))]
453fn generate_askpass_script(
454 askpass_program: &std::path::Path,
455 askpass_socket: &std::path::Path,
456) -> Result<String> {
457 let shell_kind = ShellKind::Posix;
458 let askpass_program = shell_kind.prepend_command_prefix(
459 askpass_program
460 .to_str()
461 .context("Askpass program is on a non-utf8 path")?,
462 );
463 let askpass_program = shell_kind
464 .try_quote_prefix_aware(&askpass_program)
465 .context("Failed to shell-escape Askpass program path")?;
466 let askpass_socket = askpass_socket
467 .try_shell_safe(shell_kind)
468 .context("Failed to shell-escape Askpass socket path")?;
469 let print_args = "printf '%s\\0' \"$@\"";
470 let shebang = "#!/bin/sh";
471 Ok(format!(
472 "{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
473 ))
474}
475
476#[inline]
477#[cfg(not(target_os = "windows"))]
478fn generate_gpg_wrapper_script(
479 askpass_program: &std::path::Path,
480 askpass_socket: &std::path::Path,
481) -> Result<String> {
482 let shell_kind = ShellKind::Posix;
483 let gpg_program = find_gpg_program().context("could not find a gpg binary on PATH")?;
484 let gpg_program = gpg_program
485 .to_str()
486 .context("gpg program is on a non-utf8 path")?;
487 let gpg_program = shell_kind
488 .try_quote_prefix_aware(gpg_program)
489 .context("Failed to shell-escape gpg program path")?;
490
491 let askpass_program = shell_kind.prepend_command_prefix(
492 askpass_program
493 .to_str()
494 .context("Askpass program is on a non-utf8 path")?,
495 );
496 let askpass_program = shell_kind
497 .try_quote_prefix_aware(&askpass_program)
498 .context("Failed to shell-escape Askpass program path")?;
499 let askpass_socket = askpass_socket
500 .try_shell_safe(shell_kind)
501 .context("Failed to shell-escape Askpass socket path")?;
502
503 let prompt = shell_kind
504 .try_quote_prefix_aware("Enter passphrase for your Git signing key:")
505 .context("Failed to shell-escape gpg passphrase prompt")?;
506
507 // The wrapper only intervenes when git asks gpg to *sign* (e.g. `gpg -bsau
508 // <key>`); other invocations like `--verify` run unchanged. For signing we
509 // first try plain gpg so gpg-agent/keychain can supply a cached or empty
510 // passphrase silently, and only fall back to asking Zed (loopback mode, fd
511 // 3) when that fails, e.g. the "Inappropriate ioctl for device" case with no
512 // TTY for pinentry.
513 //
514 // git streams the payload on stdin (readable once) and reads the signature
515 // from stdout, so we buffer stdin to replay it into both attempts and buffer
516 // the first attempt's output, forwarding it only if it succeeds. The
517 // passphrase goes to fd 3 via a pipe.
518 Ok(format!(
519 r#"#!/bin/sh
520for arg in "$@"; do
521 case "$arg" in
522 # Long-form signing options.
523 --sign|--detach-sign|--clearsign|--clear-sign) is_signing=1 ;;
524 # Skip other long options so flags like `--status-fd` don't match below.
525 --*) ;;
526 # Short-flag clusters containing `s`, e.g. git's `-bsau`.
527 -*s*) is_signing=1 ;;
528 esac
529done
530
531# Not a signing request: run gpg as-is, leaving its prompting untouched.
532if [ -z "${{is_signing}}" ]; then
533 exec {gpg_program} "$@"
534fi
535
536# Signing. Buffer stdin (the payload) and the first attempt's output
537# so we can retry cleanly on failure without git seeing partial output.
538tmpdir=$(mktemp -d) || exit 1
539trap 'rm -rf "$tmpdir"' EXIT
540payload="$tmpdir/payload"
541signature="$tmpdir/signature"
542status="$tmpdir/status"
543cat > "$payload" || exit 1
544
545# First try letting gpg-agent/keychain supply the passphrase without any
546# interactive pinentry. If that succeeds (cached passphrase)
547# forward its output and we're done, so Omega never shows a modal.
548if {gpg_program} --pinentry-mode error "$@" < "$payload" > "$signature" 2> "$status"; then
549 cat "$status" >&2
550 cat "$signature"
551 exit 0
552fi
553
554# The silent attempt failed: ask Omega for the passphrase, then hand it to gpg on
555# fd 3 using loopback mode so no pinentry/terminal is required.
556passphrase=$(printf '%s\0' {prompt} | {askpass_program} --askpass={askpass_socket} 2>/dev/null)
557printf '%s\n' "$passphrase" |
558{gpg_program} --pinentry-mode loopback --passphrase-fd 3 "$@" 3<&0 < "$payload"
559"#,
560 ))
561}
562
563/// Finds the real `gpg` (or `gpg2`) executable on `PATH`.
564#[inline]
565#[cfg(not(target_os = "windows"))]
566fn find_gpg_program() -> Option<std::path::PathBuf> {
567 ["gpg", "gpg2"]
568 .into_iter()
569 .find_map(|candidate| which::which(candidate).ok())
570}
571