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 && let Ok(group) = crate::ipc::process::group_target(child.id())
352 {
353 unsafe {
356 libc::kill(-group, libc::SIGKILL);
357 }
358 }
359 let _ = child.wait();
360 if let Some(guard) = guard {
361 let _ = guard.finish();
362 }
363 if !status.is_some_and(|status| status.success()) {
364 return Err("selected glab credential could not be read".into());
365 }
366 let bytes = result?;
367 let text = std::str::from_utf8(&bytes).map_err(|_| "invalid selected GitLab credential")?;
368 let token = text.trim_end_matches(['\r', '\n']);
369 if token.is_empty() || !token.bytes().all(|b| (33..=126).contains(&b)) {
370 return Err("invalid selected GitLab credential".into());
371 }
372 Ok(Zeroizing::new(token.to_owned()))
373 }
374
375 fn handoff(launch: &Launch) -> Result<Zeroizing<Vec<u8>>, String> {
379 let home = std::env::var_os("HOME").ok_or("credential home unavailable")?;
380 let token = credential(launch, std::path::Path::new(&home))?;
381 #[derive(serde::Serialize)]
382 struct Frame<'a> {
383 version: u8,
384 source: &'a Selection,
385 token: &'a str,
386 }
387 let frame = Zeroizing::new(
388 serde_json::to_vec(&Frame {
389 version: 1,
390 source: &launch.source,
391 token: &token,
392 })
393 .map_err(|_| "cannot encode credential handoff")?,
394 );
395 drop(token);
396 if frame.len() > MAX_FRAME {
397 return Err("credential handoff exceeded size limit".into());
398 }
399 Ok(frame)
400 }
401
402 pub(super) fn run(launch: Option<&Launch>, profile: &str) -> Result<i32, String> {
405 let boundary = crate::core::protected_execution::LaunchProfile::from_pinned(profile)?;
409 protected_store::validate(boundary.shell_profile())?;
410
411 let handoff_frame = match launch {
412 Some(launch) => {
413 if !launch.glab.is_absolute() || !launch.config_dir.is_absolute() {
414 return Err("selected credential paths must be absolute".into());
415 }
416 Some(handoff(launch)?)
417 }
418 None => None,
419 };
420
421 let (supervisor, worker) =
422 UnixStream::pair().map_err(|_| "cannot create the supervised execution channel")?;
423 let (mut send, receive) = match handoff_frame {
424 Some(_) => {
425 let (send, receive) =
426 UnixStream::pair().map_err(|_| "cannot create MCP credential channel")?;
427 send.set_write_timeout(Some(DEADLINE))
428 .map_err(|_| "cannot bound MCP credential channel")?;
429 send.set_read_timeout(Some(DEADLINE))
430 .map_err(|_| "cannot bound MCP acknowledgement")?;
431 (Some(send), Some(receive))
432 }
433 None => (None, None),
434 };
435
436 let credential_fd = receive.as_ref().map_or(-1, AsRawFd::as_raw_fd);
437 let worker_fd = worker.as_raw_fd();
438 let binary = std::env::current_exe().map_err(|_| "MCP executable unavailable")?;
439 let mut arguments = vec!["mcp".to_string()];
440 if receive.is_some() {
441 arguments.push(FD_FLAG.to_string());
442 arguments.push(CHILD_FD.to_string());
443 }
444 arguments.push(crate::core::protected_execution::EXEC_FD_FLAG.to_string());
445 arguments.push(crate::core::protected_execution::CHILD_EXEC_FD.to_string());
446 let mut command = Command::new(protected_store::SANDBOX_EXEC);
447 command
448 .args(["-p", profile])
449 .arg(binary)
450 .args(&arguments)
451 .env_clear();
452 for name in [
455 "HOME",
456 "LEAN_CTX_PROJECT_ROOT",
457 REQUIRED_POLICY_ROOT_ENV,
458 REQUIRED_POLICY_DIGEST_ENV,
459 protected_store::CHILD_PROFILE_ENV,
460 ]
461 .iter()
462 .chain(MCP_ENV_PASSTHROUGH.iter())
463 .chain(CLIENT_ENV_PASSTHROUGH.iter())
464 {
465 if let Some(value) = std::env::var_os(name) {
466 command.env(name, value);
467 }
468 }
469 unsafe {
474 command.pre_exec(move || {
475 let credential_hold = if credential_fd >= 0 {
476 libc::fcntl(credential_fd, libc::F_DUPFD, 10)
477 } else {
478 -1
479 };
480 if credential_fd >= 0 && credential_hold < 0 {
481 return Err(std::io::Error::last_os_error());
482 }
483 let worker_hold = libc::fcntl(worker_fd, libc::F_DUPFD, 10);
484 if worker_hold < 0 {
485 return Err(std::io::Error::last_os_error());
486 }
487 if credential_hold >= 0
488 && (libc::dup2(credential_hold, CHILD_FD) < 0
489 || libc::fcntl(CHILD_FD, libc::F_SETFD, 0) < 0)
490 {
491 return Err(std::io::Error::last_os_error());
492 }
493 if libc::dup2(worker_hold, EXEC_FD) < 0
494 || libc::fcntl(EXEC_FD, libc::F_SETFD, 0) < 0
495 {
496 return Err(std::io::Error::last_os_error());
497 }
498 if credential_hold >= 0 {
501 libc::close(credential_hold);
502 }
503 libc::close(worker_hold);
504 Ok(())
505 });
506 }
507 let mut child = command.spawn().map_err(|_| "cannot start protected MCP")?;
508 drop(receive);
509 drop(worker);
512
513 if let (Some(mut send), Some(frame)) = (send.take(), handoff_frame) {
514 let sent = send
518 .write_all(&frame)
519 .and_then(|()| send.shutdown(std::net::Shutdown::Write))
520 .and_then(|()| {
521 let mut acknowledgement = [0_u8; 1];
522 send.read_exact(&mut acknowledgement)?;
523 if acknowledgement != [1] {
524 return Err(std::io::Error::other("invalid MCP acknowledgement"));
525 }
526 Ok(())
527 });
528 drop(send);
529 drop(frame);
530 if sent.is_err() {
531 let _ = child.kill();
532 let _ = child.wait();
533 return Err("MCP credential handoff failed".into());
534 }
535 }
536
537 if let Err(error) = crate::core::protected_execution::serve(supervisor, boundary) {
541 let _ = child.kill();
542 let _ = child.wait();
543 return Err(error);
544 }
545 finish_mcp(&mut child, Duration::from_secs(5))
546 }
547
548 fn finish_mcp(child: &mut std::process::Child, grace: Duration) -> Result<i32, String> {
549 let deadline = Instant::now() + grace;
550 loop {
551 match child.try_wait() {
552 Ok(Some(status)) => return Ok(status.code().unwrap_or(1)),
553 Ok(None) if Instant::now() < deadline => {
554 std::thread::sleep(Duration::from_millis(10));
555 }
556 _ => {
557 let _ = child.kill();
558 let _ = child.wait();
559 return Err("protected execution channel ended before MCP shutdown".into());
560 }
561 }
562 }
563 }
564
565 #[test]
566 fn a_closed_execution_channel_cannot_leave_a_live_mcp_child() {
567 let mut child = Command::new("/bin/sleep").arg("120").spawn().unwrap();
568 let result = finish_mcp(&mut child, Duration::from_millis(20));
569 assert!(result.is_err());
570 assert!(child.try_wait().unwrap().is_some());
571 }
572
573 pub(super) fn receive(fd: i32) -> Result<(), String> {
574 if fd <= 2 {
575 return Err("invalid protected GitLab descriptor".into());
576 }
577 let mut kind: libc::c_int = 0;
579 let mut len = std::mem::size_of_val(&kind) as libc::socklen_t;
580 if unsafe {
582 libc::getsockopt(
583 fd,
584 libc::SOL_SOCKET,
585 libc::SO_TYPE,
586 (&raw mut kind).cast(),
587 &raw mut len,
588 )
589 } != 0
590 || kind != libc::SOCK_STREAM
591 {
592 return Err("GitLab bootstrap requires an anonymous stream socket".into());
593 }
594 let mut stream = unsafe { UnixStream::from_raw_fd(fd) };
597 if !stream
598 .local_addr()
599 .is_ok_and(|address| address.is_unnamed())
600 || !stream.peer_addr().is_ok_and(|address| address.is_unnamed())
601 {
602 return Err("GitLab bootstrap requires an anonymous connected socket pair".into());
603 }
604 if unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) } < 0 {
606 return Err("cannot seal credential descriptor".into());
607 }
608 let bytes = read_bounded(&mut stream, MAX_FRAME)?;
609 #[derive(serde::Deserialize)]
610 #[serde(deny_unknown_fields)]
611 struct Frame {
612 version: u8,
613 source: Selection,
614 #[serde(deserialize_with = "secret_string")]
615 token: Zeroizing<String>,
616 }
617 fn secret_string<'de, D: serde::Deserializer<'de>>(
618 deserializer: D,
619 ) -> Result<Zeroizing<String>, D::Error> {
620 serde::Deserialize::deserialize(deserializer).map(Zeroizing::new)
621 }
622 let frame: Frame =
623 serde_json::from_slice(&bytes).map_err(|_| "invalid GitLab bootstrap frame")?;
624 if frame.version != 1 {
625 return Err("unsupported GitLab bootstrap version".into());
626 }
627 crate::core::providers::selected_gitlab::install(frame.source, frame.token.to_string())?;
628 stream
629 .set_nonblocking(false)
630 .map_err(|_| "cannot acknowledge credential handoff")?;
631 stream
632 .set_write_timeout(Some(DEADLINE))
633 .map_err(|_| "cannot bound credential acknowledgement")?;
634 stream
635 .write_all(&[1])
636 .map_err(|_| "credential acknowledgement failed")?;
637 drop(stream);
638 Ok(())
639 }
640
641 #[cfg(test)]
642 mod tests {
643 use super::*;
644 #[test]
645 fn memory_channel_bounds_and_eof() {
646 let (mut read, mut write) = UnixStream::pair().unwrap();
647 write.write_all(b"synthetic").unwrap();
648 drop(write);
649 assert_eq!(read_bounded(&mut read, 9).unwrap().as_slice(), b"synthetic");
650 let (mut read, mut write) = UnixStream::pair().unwrap();
651 write.write_all(b"too long").unwrap();
652 drop(write);
653 assert!(read_bounded(&mut read, 2).is_err());
654 }
655 #[test]
660 fn the_credential_reader_leaves_nothing_behind() {
661 crate::shell::containment::tests::run_external_fixture(
662 module_path!(),
663 "credential_reader_fixture",
664 );
665 }
666
667 #[test]
672 #[ignore = "fixture body of the_credential_reader_leaves_nothing_behind"]
673 fn credential_reader_fixture() {
674 use std::os::unix::fs::PermissionsExt as _;
675 let fixture = crate::shell::containment::tests::external_fixture();
676 let directory = tempfile::tempdir().unwrap();
677 let glab = directory.path().join("glab");
678 let left = directory.path().join("left.pid");
679 std::fs::write(
680 &glab,
681 format!(
682 "#!/bin/sh\n/bin/sleep 20 >/dev/null 2>&1 &\necho $! > '{}'\necho not-a-credential\n",
683 left.display()
684 ),
685 )
686 .unwrap();
687 std::fs::set_permissions(&glab, std::fs::Permissions::from_mode(0o700)).unwrap();
688 let launch = Launch {
689 source: Selection {
690 host: "gitlab.example.test".into(),
691 project: 1,
692 namespace: "group".into(),
693 },
694 glab,
695 config_dir: directory.path().to_path_buf(),
696 };
697 let value = credential(&launch, directory.path()).unwrap();
698 assert_eq!(value.as_str(), "not-a-credential");
699 let pid = crate::ipc::process::parse_pid_file(&std::fs::read_to_string(&left).unwrap())
700 .expect("the reader wrote a valid PID");
701 let deadline = Instant::now() + Duration::from_secs(5);
702 while crate::ipc::process::is_alive(pid) {
703 assert!(
704 Instant::now() < deadline,
705 "the reader's background process outlived the read"
706 );
707 std::thread::sleep(Duration::from_millis(20));
708 }
709 drop(directory);
712 fixture.complete();
713 }
714
715 #[test]
719 fn the_credential_reader_never_runs_before_its_guard_is_ready() {
720 crate::shell::containment::tests::run_external_fixture(
721 module_path!(),
722 "credential_caller_killed_fixture",
723 );
724 }
725
726 #[test]
729 #[ignore = "fixture body of the_credential_reader_never_runs_before_its_guard_is_ready"]
730 fn credential_caller_killed_fixture() {
731 let fixture = crate::shell::containment::tests::external_fixture();
732 crate::shell::containment::tests::caller_killed_before_its_guard(
733 module_path!(),
734 "credential_caller_standin",
735 );
736 fixture.complete();
737 }
738
739 #[test]
743 #[ignore = "stand-in caller of credential_caller_killed_fixture"]
744 fn credential_caller_standin() {
745 use std::os::unix::fs::PermissionsExt as _;
746 let marker = crate::shell::containment::tests::caller_standin_marker();
747 let directory = marker.parent().unwrap();
748 let glab = directory.join("glab");
749 std::fs::write(
750 &glab,
751 format!(
752 "#!/bin/sh\necho ran > '{}'\n/bin/sleep 60\n",
753 marker.display()
754 ),
755 )
756 .unwrap();
757 std::fs::set_permissions(&glab, std::fs::Permissions::from_mode(0o700)).unwrap();
758 let launch = Launch {
759 source: Selection {
760 host: "gitlab.example.test".into(),
761 project: 1,
762 namespace: "group".into(),
763 },
764 glab,
765 config_dir: directory.to_path_buf(),
766 };
767 let _ = credential(&launch, directory);
768 }
769 #[test]
770 fn standard_streams_and_closed_descriptors_are_rejected() {
771 for fd in [-1, 0, 1, 2, i32::MAX] {
772 assert!(receive(fd).is_err());
773 }
774 }
775 }
776}