1use crate::core::policy::runtime::{REQUIRED_POLICY_DIGEST_ENV, REQUIRED_POLICY_ROOT_ENV};
18use crate::core::providers::selected_gitlab::Selection;
19use std::path::{Path, PathBuf};
20
21pub const CLIENT_ENV_PASSTHROUGH: &[&str] = &[
24 "DO_NOT_TRACK",
25 "PATH",
26 "TERM",
27 "TMPDIR",
28 "LANG",
29 "LC_ALL",
30 "LC_CTYPE",
31 "LC_MESSAGES",
32];
33
34pub const MCP_ENV_PASSTHROUGH: &[&str] = &[
37 "DO_NOT_TRACK",
38 "LEAN_CTX_CACHE_DIR",
39 "LEAN_CTX_CONFIG_DIR",
40 "LEAN_CTX_DATA_DIR",
41 "LEAN_CTX_STATE_DIR",
42 "LEANCTX_ORG_POLICY",
43 "XDG_CACHE_HOME",
44 "XDG_CONFIG_HOME",
45 "XDG_DATA_HOME",
46 "XDG_STATE_HOME",
47];
48
49pub fn utf8_path(path: &Path, label: &str) -> Result<String, String> {
50 path.to_str().map(str::to_string).ok_or_else(|| {
51 format!("{label} is not valid UTF-8; this launcher cannot express it in Codex config")
52 })
53}
54
55pub fn ensure_executable_file(path: &Path) -> Result<(), String> {
56 let meta = std::fs::metadata(path)
57 .map_err(|err| format!("could not stat {}: {err}", path.display()))?;
58 if !meta.is_file() {
59 return Err(format!("{} is not a regular file", path.display()));
60 }
61 #[cfg(unix)]
62 {
63 use std::os::unix::fs::PermissionsExt;
64 if meta.permissions().mode() & 0o111 == 0 {
65 return Err(format!("{} is not executable", path.display()));
66 }
67 }
68 Ok(())
69}
70
71#[cfg(windows)]
72mod windows;
73
74pub const WRAPPER: &str = "__protected-gitlab-mcp";
75const FD_FLAG: &str = "--protected-gitlab-fd";
76#[cfg(unix)]
77const MAX_FRAME: usize = 32 * 1024;
78
79#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
80#[serde(deny_unknown_fields)]
81pub struct Launch {
82 pub source: Selection,
83 pub glab: PathBuf,
84 pub config_dir: PathBuf,
85}
86
87pub fn prepare(source: Selection, glab: Option<&str>, home: &Path) -> Result<Launch, String> {
88 source.validate()?;
89 let glab = if let Some(path) = glab {
90 PathBuf::from(path)
91 } else {
92 std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default())
93 .map(|directory| directory.join("glab"))
94 .find(|path| path.is_file())
95 .ok_or("install glab or select its absolute path with --glab")?
96 }
97 .canonicalize()
98 .map_err(|_| "cannot resolve glab executable")?;
99 ensure_executable_file(&glab)?;
100 let config_dir = std::env::var_os("GLAB_CONFIG_DIR")
101 .map(PathBuf::from)
102 .unwrap_or_else(|| home.join("Library/Application Support/glab-cli"));
103 if !config_dir.is_absolute() {
104 return Err("GLAB_CONFIG_DIR must be absolute".into());
105 }
106 utf8_path(&config_dir, "glab configuration directory")?;
107 Ok(Launch {
108 source,
109 glab,
110 config_dir,
111 })
112}
113
114pub fn initialize_agent_gitlab(
117 source: Selection,
118 glab: &Path,
119 config_dir: Option<&Path>,
120 project: &Path,
121) -> Result<(), String> {
122 #[cfg(unix)]
123 {
124 let home = dirs::home_dir()
125 .and_then(|path| path.canonicalize().ok())
126 .ok_or("credential home unavailable")?;
127 let glab_path = utf8_path(glab, "glab executable")?;
128 let mut launch = prepare(source, Some(&glab_path), &home)?;
129 if let Some(path) = config_dir {
130 launch.config_dir = path.to_path_buf();
131 } else if cfg!(not(target_os = "macos")) {
132 launch.config_dir = home.join(".config/glab-cli");
133 }
134 launch.config_dir = launch
135 .config_dir
136 .canonicalize()
137 .map_err(|_| "credential configuration unavailable")?;
138 if launch.glab.starts_with(project)
140 || launch.config_dir.starts_with(project)
141 || home.starts_with(project)
142 {
143 return Err("credential authority overlaps project inputs".into());
144 }
145 unix::disable_core_dumps()?;
146 let token = unix::credential(&launch, &home)?;
147 crate::core::providers::selected_gitlab::install(launch.source, token.to_string())
148 }
149 #[cfg(windows)]
150 {
151 windows::initialize(source, glab, config_dir, project)
152 }
153 #[cfg(not(any(unix, windows)))]
154 {
155 let _ = (source, glab, config_dir, project);
156 Err("selected GitLab SDK credential transport unavailable on this platform".into())
157 }
158}
159
160pub fn dispatch(args: &[String]) -> Result<Option<i32>, String> {
162 const EXEC_FD_FLAG: &str = "--protected-exec-fd";
163
164 let wrapper = args.get(1).is_some_and(|arg| arg == WRAPPER);
165 let receiver = args.get(1).is_some_and(|arg| arg == "mcp")
166 && args.iter().any(|arg| arg == FD_FLAG || arg == EXEC_FD_FLAG);
167 if !wrapper && !receiver {
168 return Ok(None);
169 }
170 if !cfg!(target_os = "macos") {
171 return Err("protected GitLab bootstrap is qualified on macOS only".into());
172 }
173 if std::env::var(REQUIRED_POLICY_ROOT_ENV).is_err()
174 || std::env::var(REQUIRED_POLICY_DIGEST_ENV).is_err()
175 {
176 return Err("protected GitLab bootstrap requires the selected policy authority".into());
177 }
178 #[cfg(unix)]
179 {
180 unix::disable_core_dumps()?;
181 if wrapper {
182 if args.len() != 4 {
183 return Err("invalid protected wrapper arguments".into());
184 }
185 let launch: Option<Launch> = serde_json::from_str(&args[2])
188 .map_err(|_| "invalid protected launch configuration")?;
189 if let Some(launch) = &launch {
190 launch.source.validate()?;
191 }
192 return unix::run(launch.as_ref(), &args[3]).map(Some);
193 }
194 let mut index = 2;
198 let mut credential: Option<i32> = None;
199 let mut execution: Option<i32> = None;
200 while index < args.len() {
201 let fd: i32 = args
202 .get(index + 1)
203 .ok_or("invalid protected descriptor arguments")?
204 .parse()
205 .map_err(|_| "invalid protected descriptor")?;
206 let slot = match args[index].as_str() {
207 FD_FLAG => &mut credential,
208 EXEC_FD_FLAG => &mut execution,
209 _ => return Err("invalid protected descriptor arguments".into()),
210 };
211 if slot.is_some() {
212 return Err("duplicate protected descriptor".into());
213 }
214 *slot = Some(fd);
215 index += 2;
216 }
217 if let Some(fd) = credential {
218 unix::receive(fd)?;
219 }
220 let execution =
221 execution.ok_or("protected MCP start without a supervised execution channel")?;
222 crate::core::protected_execution::install_client(execution)?;
223 Ok(None)
224 }
225 #[cfg(not(unix))]
226 Err("protected GitLab descriptor transport unavailable".into())
227}
228
229#[cfg(unix)]
230mod unix {
231 use super::{CLIENT_ENV_PASSTHROUGH, MCP_ENV_PASSTHROUGH};
232 use super::{
233 FD_FLAG, Launch, MAX_FRAME, REQUIRED_POLICY_DIGEST_ENV, REQUIRED_POLICY_ROOT_ENV, Selection,
234 };
235 use crate::core::protected_store;
236 use std::io::{Read, Write};
237 use std::os::fd::{AsRawFd, FromRawFd};
238 use std::os::unix::net::UnixStream;
239 use std::os::unix::process::CommandExt;
240 use std::process::{Command, Stdio};
241 use std::time::{Duration, Instant};
242 use zeroize::Zeroizing;
243
244 const DEADLINE: Duration = Duration::from_secs(10);
245 const CHILD_FD: i32 = 3;
247 const EXEC_FD: i32 = crate::core::protected_execution::CHILD_EXEC_FD;
249
250 pub(super) fn disable_core_dumps() -> Result<(), String> {
251 let limit = libc::rlimit {
252 rlim_cur: 0,
253 rlim_max: 0,
254 };
255 if unsafe { libc::setrlimit(libc::RLIMIT_CORE, &raw const limit) } != 0 {
258 return Err("cannot disable credential-process core dumps".into());
259 }
260 Ok(())
261 }
262
263 fn read_bounded(stream: &mut UnixStream, max: usize) -> Result<Zeroizing<Vec<u8>>, String> {
264 stream
265 .set_nonblocking(true)
266 .map_err(|_| "cannot bound credential channel")?;
267 let end = Instant::now() + DEADLINE;
268 let mut output = Zeroizing::new(Vec::new());
269 let mut buffer = Zeroizing::new([0_u8; 4096]);
270 loop {
271 if Instant::now() >= end {
272 return Err("credential channel timed out".into());
273 }
274 match stream.read(buffer.as_mut()) {
275 Ok(0) => return Ok(output),
276 Ok(count) => {
277 if output.len() + count > max {
278 return Err("credential channel exceeded size limit".into());
279 }
280 output.extend_from_slice(&buffer[..count]);
281 }
282 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
283 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
284 std::thread::sleep(Duration::from_millis(5));
285 }
286 Err(_) => return Err("credential channel failed".into()),
287 }
288 }
289 }
290
291 pub(super) fn credential(
293 launch: &Launch,
294 home: &std::path::Path,
295 ) -> Result<Zeroizing<String>, String> {
296 let (mut read, write) =
297 UnixStream::pair().map_err(|_| "cannot create credential channel")?;
298 let mut reader = Command::new(&launch.glab);
299 reader
300 .args([
301 "config",
302 "get",
303 "token",
304 "--global",
305 "--host",
306 &launch.source.host,
307 ])
308 .env_clear()
309 .env("HOME", home)
310 .env("GLAB_CONFIG_DIR", &launch.config_dir)
311 .env("GLAB_SEND_TELEMETRY", "false")
312 .env("GLAB_CHECK_UPDATE", "false")
313 .current_dir("/")
314 .stdin(Stdio::null())
315 .stdout(Stdio::from(std::os::fd::OwnedFd::from(write)))
316 .stderr(Stdio::null())
317 .process_group(0);
318 let (mut child, guard) = match crate::shell::containment::start_guarded_child(&mut reader) {
326 Ok(started) => started,
327 Err(crate::shell::containment::Refusal::NotStarted(_)) => {
328 return Err("cannot start selected glab credential reader".into());
329 }
330 Err(_) => return Err("cannot contain selected glab credential reader".into()),
331 };
332 drop(reader);
335 let end = Instant::now() + DEADLINE;
336 let result = read_bounded(&mut read, 16 * 1024);
337 let status = loop {
338 match child.try_wait() {
339 Ok(Some(status)) => break Some(status),
340 Ok(None) if Instant::now() < end && result.is_ok() => {
341 std::thread::sleep(Duration::from_millis(5));
342 }
343 _ => break None,
344 }
345 };
346 if status.is_none() {
351 unsafe {
353 libc::kill(-(child.id() as i32), libc::SIGKILL);
354 }
355 }
356 let _ = child.wait();
357 if let Some(guard) = guard {
358 let _ = guard.finish();
359 }
360 if !status.is_some_and(|status| status.success()) {
361 return Err("selected glab credential could not be read".into());
362 }
363 let bytes = result?;
364 let text = std::str::from_utf8(&bytes).map_err(|_| "invalid selected GitLab credential")?;
365 let token = text.trim_end_matches(['\r', '\n']);
366 if token.is_empty() || !token.bytes().all(|b| (33..=126).contains(&b)) {
367 return Err("invalid selected GitLab credential".into());
368 }
369 Ok(Zeroizing::new(token.to_owned()))
370 }
371
372 fn handoff(launch: &Launch) -> Result<Zeroizing<Vec<u8>>, String> {
376 let home = std::env::var_os("HOME").ok_or("credential home unavailable")?;
377 let token = credential(launch, std::path::Path::new(&home))?;
378 #[derive(serde::Serialize)]
379 struct Frame<'a> {
380 version: u8,
381 source: &'a Selection,
382 token: &'a str,
383 }
384 let frame = Zeroizing::new(
385 serde_json::to_vec(&Frame {
386 version: 1,
387 source: &launch.source,
388 token: &token,
389 })
390 .map_err(|_| "cannot encode credential handoff")?,
391 );
392 drop(token);
393 if frame.len() > MAX_FRAME {
394 return Err("credential handoff exceeded size limit".into());
395 }
396 Ok(frame)
397 }
398
399 pub(super) fn run(launch: Option<&Launch>, profile: &str) -> Result<i32, String> {
402 let boundary = crate::core::protected_execution::LaunchProfile::from_pinned(profile)?;
406 protected_store::validate(boundary.shell_profile())?;
407
408 let handoff_frame = match launch {
409 Some(launch) => {
410 if !launch.glab.is_absolute() || !launch.config_dir.is_absolute() {
411 return Err("selected credential paths must be absolute".into());
412 }
413 Some(handoff(launch)?)
414 }
415 None => None,
416 };
417
418 let (supervisor, worker) =
419 UnixStream::pair().map_err(|_| "cannot create the supervised execution channel")?;
420 let (mut send, receive) = match handoff_frame {
421 Some(_) => {
422 let (send, receive) =
423 UnixStream::pair().map_err(|_| "cannot create MCP credential channel")?;
424 send.set_write_timeout(Some(DEADLINE))
425 .map_err(|_| "cannot bound MCP credential channel")?;
426 send.set_read_timeout(Some(DEADLINE))
427 .map_err(|_| "cannot bound MCP acknowledgement")?;
428 (Some(send), Some(receive))
429 }
430 None => (None, None),
431 };
432
433 let credential_fd = receive.as_ref().map_or(-1, AsRawFd::as_raw_fd);
434 let worker_fd = worker.as_raw_fd();
435 let binary = std::env::current_exe().map_err(|_| "MCP executable unavailable")?;
436 let mut arguments = vec!["mcp".to_string()];
437 if receive.is_some() {
438 arguments.push(FD_FLAG.to_string());
439 arguments.push(CHILD_FD.to_string());
440 }
441 arguments.push(crate::core::protected_execution::EXEC_FD_FLAG.to_string());
442 arguments.push(crate::core::protected_execution::CHILD_EXEC_FD.to_string());
443 let mut command = Command::new(protected_store::SANDBOX_EXEC);
444 command
445 .args(["-p", profile])
446 .arg(binary)
447 .args(&arguments)
448 .env_clear();
449 for name in [
452 "HOME",
453 "LEAN_CTX_PROJECT_ROOT",
454 REQUIRED_POLICY_ROOT_ENV,
455 REQUIRED_POLICY_DIGEST_ENV,
456 protected_store::CHILD_PROFILE_ENV,
457 ]
458 .iter()
459 .chain(MCP_ENV_PASSTHROUGH.iter())
460 .chain(CLIENT_ENV_PASSTHROUGH.iter())
461 {
462 if let Some(value) = std::env::var_os(name) {
463 command.env(name, value);
464 }
465 }
466 unsafe {
471 command.pre_exec(move || {
472 let credential_hold = if credential_fd >= 0 {
473 libc::fcntl(credential_fd, libc::F_DUPFD, 10)
474 } else {
475 -1
476 };
477 if credential_fd >= 0 && credential_hold < 0 {
478 return Err(std::io::Error::last_os_error());
479 }
480 let worker_hold = libc::fcntl(worker_fd, libc::F_DUPFD, 10);
481 if worker_hold < 0 {
482 return Err(std::io::Error::last_os_error());
483 }
484 if credential_hold >= 0
485 && (libc::dup2(credential_hold, CHILD_FD) < 0
486 || libc::fcntl(CHILD_FD, libc::F_SETFD, 0) < 0)
487 {
488 return Err(std::io::Error::last_os_error());
489 }
490 if libc::dup2(worker_hold, EXEC_FD) < 0
491 || libc::fcntl(EXEC_FD, libc::F_SETFD, 0) < 0
492 {
493 return Err(std::io::Error::last_os_error());
494 }
495 if credential_hold >= 0 {
498 libc::close(credential_hold);
499 }
500 libc::close(worker_hold);
501 Ok(())
502 });
503 }
504 let mut child = command.spawn().map_err(|_| "cannot start protected MCP")?;
505 drop(receive);
506 drop(worker);
509
510 if let (Some(mut send), Some(frame)) = (send.take(), handoff_frame) {
511 let sent = send
515 .write_all(&frame)
516 .and_then(|()| send.shutdown(std::net::Shutdown::Write))
517 .and_then(|()| {
518 let mut acknowledgement = [0_u8; 1];
519 send.read_exact(&mut acknowledgement)?;
520 if acknowledgement != [1] {
521 return Err(std::io::Error::other("invalid MCP acknowledgement"));
522 }
523 Ok(())
524 });
525 drop(send);
526 drop(frame);
527 if sent.is_err() {
528 let _ = child.kill();
529 let _ = child.wait();
530 return Err("MCP credential handoff failed".into());
531 }
532 }
533
534 if let Err(error) = crate::core::protected_execution::serve(supervisor, boundary) {
538 let _ = child.kill();
539 let _ = child.wait();
540 return Err(error);
541 }
542 finish_mcp(&mut child, Duration::from_secs(5))
543 }
544
545 fn finish_mcp(child: &mut std::process::Child, grace: Duration) -> Result<i32, String> {
546 let deadline = Instant::now() + grace;
547 loop {
548 match child.try_wait() {
549 Ok(Some(status)) => return Ok(status.code().unwrap_or(1)),
550 Ok(None) if Instant::now() < deadline => {
551 std::thread::sleep(Duration::from_millis(10));
552 }
553 _ => {
554 let _ = child.kill();
555 let _ = child.wait();
556 return Err("protected execution channel ended before MCP shutdown".into());
557 }
558 }
559 }
560 }
561
562 #[test]
563 fn a_closed_execution_channel_cannot_leave_a_live_mcp_child() {
564 let mut child = Command::new("/bin/sleep").arg("120").spawn().unwrap();
565 let result = finish_mcp(&mut child, Duration::from_millis(20));
566 assert!(result.is_err());
567 assert!(child.try_wait().unwrap().is_some());
568 }
569
570 pub(super) fn receive(fd: i32) -> Result<(), String> {
571 if fd <= 2 {
572 return Err("invalid protected GitLab descriptor".into());
573 }
574 let mut kind: libc::c_int = 0;
576 let mut len = std::mem::size_of_val(&kind) as libc::socklen_t;
577 if unsafe {
579 libc::getsockopt(
580 fd,
581 libc::SOL_SOCKET,
582 libc::SO_TYPE,
583 (&raw mut kind).cast(),
584 &raw mut len,
585 )
586 } != 0
587 || kind != libc::SOCK_STREAM
588 {
589 return Err("GitLab bootstrap requires an anonymous stream socket".into());
590 }
591 let mut stream = unsafe { UnixStream::from_raw_fd(fd) };
594 if !stream
595 .local_addr()
596 .is_ok_and(|address| address.is_unnamed())
597 || !stream.peer_addr().is_ok_and(|address| address.is_unnamed())
598 {
599 return Err("GitLab bootstrap requires an anonymous connected socket pair".into());
600 }
601 if unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) } < 0 {
603 return Err("cannot seal credential descriptor".into());
604 }
605 let bytes = read_bounded(&mut stream, MAX_FRAME)?;
606 #[derive(serde::Deserialize)]
607 #[serde(deny_unknown_fields)]
608 struct Frame {
609 version: u8,
610 source: Selection,
611 #[serde(deserialize_with = "secret_string")]
612 token: Zeroizing<String>,
613 }
614 fn secret_string<'de, D: serde::Deserializer<'de>>(
615 deserializer: D,
616 ) -> Result<Zeroizing<String>, D::Error> {
617 serde::Deserialize::deserialize(deserializer).map(Zeroizing::new)
618 }
619 let frame: Frame =
620 serde_json::from_slice(&bytes).map_err(|_| "invalid GitLab bootstrap frame")?;
621 if frame.version != 1 {
622 return Err("unsupported GitLab bootstrap version".into());
623 }
624 crate::core::providers::selected_gitlab::install(frame.source, frame.token.to_string())?;
625 stream
626 .set_nonblocking(false)
627 .map_err(|_| "cannot acknowledge credential handoff")?;
628 stream
629 .set_write_timeout(Some(DEADLINE))
630 .map_err(|_| "cannot bound credential acknowledgement")?;
631 stream
632 .write_all(&[1])
633 .map_err(|_| "credential acknowledgement failed")?;
634 drop(stream);
635 Ok(())
636 }
637
638 #[cfg(test)]
639 mod tests {
640 use super::*;
641 #[test]
642 fn memory_channel_bounds_and_eof() {
643 let (mut read, mut write) = UnixStream::pair().unwrap();
644 write.write_all(b"synthetic").unwrap();
645 drop(write);
646 assert_eq!(read_bounded(&mut read, 9).unwrap().as_slice(), b"synthetic");
647 let (mut read, mut write) = UnixStream::pair().unwrap();
648 write.write_all(b"too long").unwrap();
649 drop(write);
650 assert!(read_bounded(&mut read, 2).is_err());
651 }
652 #[test]
657 fn the_credential_reader_leaves_nothing_behind() {
658 crate::shell::containment::tests::run_external_fixture(
659 module_path!(),
660 "credential_reader_fixture",
661 );
662 }
663
664 #[test]
669 #[ignore = "fixture body of the_credential_reader_leaves_nothing_behind"]
670 fn credential_reader_fixture() {
671 use std::os::unix::fs::PermissionsExt as _;
672 let fixture = crate::shell::containment::tests::external_fixture();
673 let directory = tempfile::tempdir().unwrap();
674 let glab = directory.path().join("glab");
675 let left = directory.path().join("left.pid");
676 std::fs::write(
677 &glab,
678 format!(
679 "#!/bin/sh\n/bin/sleep 20 >/dev/null 2>&1 &\necho $! > '{}'\necho not-a-credential\n",
680 left.display()
681 ),
682 )
683 .unwrap();
684 std::fs::set_permissions(&glab, std::fs::Permissions::from_mode(0o700)).unwrap();
685 let launch = Launch {
686 source: Selection {
687 host: "gitlab.example.test".into(),
688 project: 1,
689 namespace: "group".into(),
690 },
691 glab,
692 config_dir: directory.path().to_path_buf(),
693 };
694 let value = credential(&launch, directory.path()).unwrap();
695 assert_eq!(value.as_str(), "not-a-credential");
696 let pid: libc::pid_t = std::fs::read_to_string(&left)
697 .unwrap()
698 .trim()
699 .parse()
700 .unwrap();
701 let deadline = Instant::now() + Duration::from_secs(5);
702 while unsafe { libc::kill(pid, 0) } == 0 {
704 assert!(
705 Instant::now() < deadline,
706 "the reader's background process outlived the read"
707 );
708 std::thread::sleep(Duration::from_millis(20));
709 }
710 drop(directory);
713 fixture.complete();
714 }
715
716 #[test]
720 fn the_credential_reader_never_runs_before_its_guard_is_ready() {
721 crate::shell::containment::tests::run_external_fixture(
722 module_path!(),
723 "credential_caller_killed_fixture",
724 );
725 }
726
727 #[test]
730 #[ignore = "fixture body of the_credential_reader_never_runs_before_its_guard_is_ready"]
731 fn credential_caller_killed_fixture() {
732 let fixture = crate::shell::containment::tests::external_fixture();
733 crate::shell::containment::tests::caller_killed_before_its_guard(
734 module_path!(),
735 "credential_caller_standin",
736 );
737 fixture.complete();
738 }
739
740 #[test]
744 #[ignore = "stand-in caller of credential_caller_killed_fixture"]
745 fn credential_caller_standin() {
746 use std::os::unix::fs::PermissionsExt as _;
747 let marker = crate::shell::containment::tests::caller_standin_marker();
748 let directory = marker.parent().unwrap();
749 let glab = directory.join("glab");
750 std::fs::write(
751 &glab,
752 format!(
753 "#!/bin/sh\necho ran > '{}'\n/bin/sleep 60\n",
754 marker.display()
755 ),
756 )
757 .unwrap();
758 std::fs::set_permissions(&glab, std::fs::Permissions::from_mode(0o700)).unwrap();
759 let launch = Launch {
760 source: Selection {
761 host: "gitlab.example.test".into(),
762 project: 1,
763 namespace: "group".into(),
764 },
765 glab,
766 config_dir: directory.to_path_buf(),
767 };
768 let _ = credential(&launch, directory);
769 }
770 #[test]
771 fn standard_streams_and_closed_descriptors_are_rejected() {
772 for fd in [-1, 0, 1, 2, i32::MAX] {
773 assert!(receive(fd).is_err());
774 }
775 }
776 }
777}