1use std::{
9 ffi::OsStr,
10 path::{Path, PathBuf},
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Login {
16 Command(&'static [&'static str]),
18 Interactive {
20 args: &'static [&'static str],
21 hint: &'static str,
22 },
23 ApiKey(KeyStore),
25 Import,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Status {
32 Command(&'static [&'static str]),
34 Stored(KeyStore),
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum OutputFormat {
42 Text,
44 ClaudeStreamJson,
47 CodexJsonl,
50 PiJson,
53}
54
55impl OutputFormat {
56 pub fn args(self) -> &'static [&'static str] {
58 match self {
59 Self::Text => &[],
60 Self::ClaudeStreamJson => &["--output-format", "stream-json", "--verbose"],
61 Self::CodexJsonl => &["--json"],
62 Self::PiJson => &["--mode", "json"],
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum Transport {
70 Process,
73 ScvProtocol,
77}
78
79impl Transport {
80 pub fn is_live(self) -> bool {
82 !matches!(self, Self::Process)
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub struct AcpLaunch {
90 pub command: &'static str,
92 pub args: &'static [&'static str],
95 pub full_args: &'static [&'static str],
96 pub full_mode: Option<&'static str>,
99 pub full_environment: &'static [(&'static str, &'static str)],
102}
103
104pub fn acp_args(launch: &AcpLaunch, full: bool) -> Vec<String> {
106 let mut args = Vec::with_capacity(launch.args.len() + launch.full_args.len());
107 for arg in launch.args {
108 if *arg == "{full}" {
109 if full {
110 args.extend(launch.full_args.iter().map(|arg| (*arg).to_owned()));
111 }
112 } else {
113 args.push((*arg).to_owned());
114 }
115 }
116 args
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum Resume {
123 Unsupported,
125 Supported {
126 start: &'static [&'static str],
129 subcommand: &'static [&'static str],
132 options: &'static [&'static str],
134 positional: &'static [&'static str],
137 },
138}
139
140impl Resume {
141 pub fn is_supported(self) -> bool {
142 matches!(self, Self::Supported { .. })
143 }
144
145 pub fn assigns_id(self) -> bool {
147 matches!(self, Self::Supported { start, .. } if !start.is_empty())
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub struct ConversationFiles {
155 pub dir: &'static str,
156 pub extension: &'static str,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum StatusSummary {
163 ClaudeJson,
165 CodexText,
167 ExitStatus,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum Logout {
174 Command(&'static [&'static str]),
175 Stored(KeyStore),
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub enum KeyStore {
182 Grok {
185 auth: &'static str,
186 config: &'static str,
187 },
188 DshRefs {
190 path: &'static str,
191 variable: &'static str,
192 },
193 Pi { dir: &'static str },
196 Scv { config: &'static str },
199}
200
201#[derive(Debug, Clone, Copy)]
202pub struct AdapterDescriptor {
203 pub name: &'static str,
205 pub product: &'static str,
207 pub offers: &'static str,
210 pub command: &'static str,
211 pub args: &'static [&'static str],
212 pub prompt_args: &'static [&'static str],
215 pub model_args: &'static [&'static str],
216 pub effort_args: &'static [&'static str],
217 pub model_hint: &'static str,
219 pub home_environment: &'static [(&'static str, &'static str)],
222 pub fixed_environment: &'static [(&'static str, &'static str)],
224 pub removed_environment: &'static [&'static str],
227 pub full_permission_args: &'static [&'static str],
232 pub full_permission_environment: &'static [(&'static str, &'static str)],
234 pub search_dirs: &'static [&'static str],
239 pub login: Login,
240 pub status: Status,
241 pub status_summary: StatusSummary,
243 pub logout: Logout,
244 pub output: OutputFormat,
246 pub resume: Resume,
248 pub conversation_files: Option<ConversationFiles>,
250 pub credential_files: &'static [&'static str],
253 pub transport: Transport,
255 pub acp: Option<AcpLaunch>,
259}
260
261const USER_BIN_DIRS: &[&str] = &[".local/bin"];
263
264const COMMON_REMOVED_ENVIRONMENT: &[&str] = &[
268 "SCV_CONFIG",
269 "SCV_MODEL",
270 "SCV_PROVIDER",
271 "SCV_BASE_URL",
272 "SCV_API_KEY_ENV",
273 "GEMINI_API_KEY",
274 "GOOGLE_API_KEY",
275 "AZURE_OPENAI_API_KEY",
276 "AZURE_OPENAI_ENDPOINT",
277];
278
279const PI_STORE: KeyStore = KeyStore::Pi { dir: ".pi/agent" };
280const SCV_STORE: KeyStore = KeyStore::Scv {
281 config: "config.toml",
282};
283const DSH_STORE: KeyStore = KeyStore::DshRefs {
284 path: ".dsh/.credentials.yaml",
285 variable: "DEEPSEEK_API_KEY",
286};
287
288pub const ADAPTERS: &[AdapterDescriptor] = &[
289 AdapterDescriptor {
290 name: "claude",
291 product: "Claude Code",
292 offers: "Anthropic's coding agent; it reads, edits, and runs code in a project and can search and fetch the web",
293 command: "claude",
294 args: &["-p"],
295 prompt_args: &[],
296 model_args: &["--model", "{model}"],
297 effort_args: &["--effort", "{effort}"],
298 model_hint: "Claude model alias or ID, such as sonnet or opus.",
299 home_environment: &[],
300 fixed_environment: &[],
301 removed_environment: &[
302 "ANTHROPIC_API_KEY",
303 "ANTHROPIC_BASE_URL",
304 "ANTHROPIC_AUTH_TOKEN",
305 "CLAUDE_CODE_OAUTH_TOKEN",
306 "CLAUDE_CONFIG_DIR",
307 ],
308 full_permission_args: &["--permission-mode", "bypassPermissions"],
310 full_permission_environment: &[],
311 search_dirs: &[],
312 login: Login::Command(&["auth", "login"]),
313 status: Status::Command(&["auth", "status"]),
314 status_summary: StatusSummary::ClaudeJson,
315 logout: Logout::Command(&["auth", "logout"]),
316 output: OutputFormat::ClaudeStreamJson,
317 resume: Resume::Supported {
319 start: &["--session-id", "{session}"],
320 subcommand: &[],
321 options: &["--resume", "{session}"],
322 positional: &[],
323 },
324 conversation_files: Some(ConversationFiles {
325 dir: ".claude/projects",
326 extension: "jsonl",
327 }),
328 credential_files: &[".claude/.credentials.json"],
329 transport: Transport::Process,
330 acp: Some(AcpLaunch {
333 command: "claude-agent-acp",
334 args: &[],
335 full_args: &[],
336 full_mode: Some("bypassPermissions"),
337 full_environment: &[],
338 }),
339 },
340 AdapterDescriptor {
341 name: "codex",
342 product: "Codex",
343 offers: "OpenAI's coding agent; it reads, edits, and runs code in a project, with live web search under full permissions",
344 command: "codex",
345 args: &["exec"],
346 prompt_args: &[],
347 model_args: &["-m", "{model}"],
348 effort_args: &["-c", "model_reasoning_effort=\"{effort}\""],
349 model_hint: "OpenAI model ID from the Codex configuration; not a Claude alias.",
350 home_environment: &[("CODEX_HOME", "")],
351 fixed_environment: &[],
352 removed_environment: &[
353 "OPENAI_API_KEY",
354 "OPENAI_BASE_URL",
355 "OPENAI_ORG_ID",
356 "OPENAI_PROJECT_ID",
357 "CODEX_API_KEY",
358 "CODEX_BASE_URL",
359 "CODEX_CONFIG",
360 ],
361 full_permission_args: &[
363 "--dangerously-bypass-approvals-and-sandbox",
364 "-c",
365 "web_search=\"live\"",
366 ],
367 full_permission_environment: &[],
368 search_dirs: &[],
369 login: Login::Command(&["login"]),
370 status: Status::Command(&["login", "status"]),
371 status_summary: StatusSummary::CodexText,
372 logout: Logout::Command(&["logout"]),
373 output: OutputFormat::CodexJsonl,
374 resume: Resume::Supported {
377 start: &[],
378 subcommand: &["resume"],
379 options: &[],
380 positional: &["{session}"],
381 },
382 conversation_files: Some(ConversationFiles {
383 dir: "sessions",
384 extension: "jsonl",
385 }),
386 credential_files: &["auth.json"],
387 transport: Transport::Process,
388 acp: Some(AcpLaunch {
394 command: "codex-acp",
395 args: &[],
396 full_args: &[],
397 full_mode: Some("agent-full-access"),
398 full_environment: &[("CODEX_CONFIG", r#"{"web_search":"live"}"#)],
399 }),
400 },
401 AdapterDescriptor {
402 name: "grok",
403 product: "Grok Build",
404 offers: "xAI's coding agent; it reads, edits, and runs code in a project and has live web and X search",
405 command: "grok",
406 args: &[],
407 prompt_args: &["-p"],
408 model_args: &["-m", "{model}"],
409 effort_args: &["--reasoning-effort", "{effort}"],
410 model_hint: "xAI Grok model ID, such as grok-4.7.",
411 home_environment: &[("GROK_HOME", ".grok")],
412 fixed_environment: &[("GROK_DISABLE_AUTOUPDATER", "1")],
413 removed_environment: &["GROK_*", "XAI_API_KEY"],
414 full_permission_args: &["--always-approve"],
416 full_permission_environment: &[],
417 search_dirs: &[".grok/bin"],
418 login: Login::Command(&["login"]),
419 status: Status::Stored(KeyStore::Grok {
420 auth: ".grok/auth.json",
421 config: ".grok/config.toml",
422 }),
423 status_summary: StatusSummary::ExitStatus,
424 logout: Logout::Command(&["logout"]),
425 output: OutputFormat::Text,
427 resume: Resume::Unsupported,
430 conversation_files: None,
431 credential_files: &[".grok/auth.json", ".grok/config.toml"],
432 transport: Transport::Process,
433 acp: Some(AcpLaunch {
435 command: "grok",
436 args: &["agent", "{full}", "stdio"],
437 full_args: &["--always-approve"],
438 full_mode: None,
439 full_environment: &[],
440 }),
441 },
442 AdapterDescriptor {
443 name: "dsh",
444 product: "DeepSeek Harness",
445 offers: "a coding agent on DeepSeek models; it reads, edits, and runs code in a project",
446 command: "dsh",
447 args: &["--profile", "headless"],
448 prompt_args: &[],
449 model_args: &[],
450 effort_args: &[],
451 model_hint: "Model ID in the form this agent's CLI accepts.",
452 home_environment: &[("DSH_HOME", ".dsh")],
453 fixed_environment: &[],
454 removed_environment: &["DSH_*", "DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL"],
455 full_permission_args: &[],
457 full_permission_environment: &[("DSH_PERMISSION_MODE", "danger-full-access")],
458 search_dirs: &[],
459 login: Login::ApiKey(DSH_STORE),
460 status: Status::Stored(DSH_STORE),
461 status_summary: StatusSummary::ExitStatus,
462 logout: Logout::Stored(DSH_STORE),
463 output: OutputFormat::Text,
464 resume: Resume::Unsupported,
466 conversation_files: None,
467 credential_files: &[".dsh/.credentials.yaml"],
468 transport: Transport::Process,
469 acp: Some(AcpLaunch {
472 command: "dsh",
473 args: &["--profile", "acp"],
474 full_args: &[],
475 full_mode: None,
476 full_environment: &[],
477 }),
478 },
479 AdapterDescriptor {
480 name: "pi",
481 product: "pi",
482 offers: "a minimal coding agent (read, write, edit, bash) that can run on SCV's own model endpoint; it has no web search",
483 command: "pi",
484 args: &["-p"],
485 prompt_args: &[],
486 model_args: &["--model", "{model}"],
487 effort_args: &["--thinking", "{effort}"],
488 model_hint: "pi model pattern or provider/id; the SCV-configured endpoint is provider scv.",
489 home_environment: &[("PI_CODING_AGENT_DIR", ".pi/agent")],
490 fixed_environment: &[],
491 removed_environment: &["PI_*"],
492 full_permission_args: &[],
494 full_permission_environment: &[],
495 search_dirs: &[],
496 login: Login::Interactive {
497 args: &[],
498 hint: "run /login and choose a provider, then /quit",
499 },
500 status: Status::Stored(PI_STORE),
501 status_summary: StatusSummary::ExitStatus,
502 logout: Logout::Stored(PI_STORE),
503 output: OutputFormat::PiJson,
504 resume: Resume::Supported {
506 start: &["--session-id", "{session}"],
507 subcommand: &[],
508 options: &["--session-id", "{session}"],
509 positional: &[],
510 },
511 conversation_files: Some(ConversationFiles {
512 dir: ".pi/agent/sessions",
513 extension: "jsonl",
514 }),
515 credential_files: &[".pi/agent/auth.json", ".pi/agent/models.json"],
516 transport: Transport::Process,
517 acp: None,
519 },
520 AdapterDescriptor {
521 name: "scv",
522 product: "SCV",
523 offers: "a nested SCV session with its own context and tools; suited to a self-contained sub-task kept out of this conversation's context, or work in another project",
524 command: "scv",
525 args: &["server", "--stdio"],
526 prompt_args: &[],
527 model_args: &[],
529 effort_args: &[],
530 model_hint: "Model ID for the nested SCV's provider; applies to a new conversation only.",
531 home_environment: &[],
534 fixed_environment: &[],
535 removed_environment: &[],
536 full_permission_args: &[],
538 full_permission_environment: &[],
539 search_dirs: &[".cargo/bin"],
541 login: Login::Import,
542 status: Status::Stored(SCV_STORE),
543 status_summary: StatusSummary::ExitStatus,
544 logout: Logout::Stored(SCV_STORE),
545 output: OutputFormat::Text,
546 resume: Resume::Unsupported,
547 conversation_files: None,
548 credential_files: &["config.toml"],
549 transport: Transport::ScvProtocol,
550 acp: None,
551 },
552];
553
554pub fn adapter(name: &str) -> Option<&'static AdapterDescriptor> {
556 ADAPTERS.iter().find(|adapter| adapter.name == name)
557}
558
559pub fn is_removed_agent_variable(variable: &OsStr) -> bool {
563 let Some(variable) = variable.to_str() else {
564 return false;
565 };
566 variable.ends_with("_API_KEY")
567 || COMMON_REMOVED_ENVIRONMENT.contains(&variable)
568 || ADAPTERS
569 .iter()
570 .flat_map(|adapter| adapter.removed_environment)
571 .any(|rule| match rule.strip_suffix('*') {
572 Some(prefix) => variable.starts_with(prefix),
573 None => variable == *rule,
574 })
575}
576
577pub fn summarize_status(summary: StatusSummary, succeeded: bool, output: &str) -> String {
580 let signed_out = "not signed in".to_owned();
581 match summary {
582 StatusSummary::ClaudeJson => {
583 let first = serde_json::Deserializer::from_str(output)
585 .into_iter::<serde_json::Value>()
586 .next();
587 let Some(Ok(value)) = first else {
588 return if succeeded {
589 "signed in".into()
590 } else {
591 signed_out
592 };
593 };
594 if value.get("loggedIn").and_then(serde_json::Value::as_bool) != Some(true) {
595 return signed_out;
596 }
597 let method = match value.get("authMethod").and_then(serde_json::Value::as_str) {
598 Some("claude.ai") => "Claude account",
599 Some("api_key" | "apiKey" | "console") => "API key",
600 Some("oauth_token" | "oauthToken") => "OAuth token",
601 _ => "other method",
602 };
603 match value
604 .get("subscriptionType")
605 .and_then(serde_json::Value::as_str)
606 .filter(|plan| ["free", "pro", "max", "team", "enterprise"].contains(plan))
607 {
608 Some(plan) => format!("signed in ({method}, {plan})"),
609 None => format!("signed in ({method})"),
610 }
611 }
612 StatusSummary::CodexText => {
613 let lower = output.to_ascii_lowercase();
614 if !succeeded || lower.contains("not logged in") {
615 signed_out
616 } else if lower.contains("api key") {
617 "signed in (API key)".into()
618 } else if lower.contains("chatgpt") {
619 "signed in (ChatGPT account)".into()
620 } else {
621 "signed in".into()
622 }
623 }
624 StatusSummary::ExitStatus => {
625 if succeeded {
626 "signed in".into()
627 } else {
628 signed_out
629 }
630 }
631 }
632}
633
634pub fn resolve_agent_executable(command: &str, search_dirs: &[PathBuf]) -> Option<PathBuf> {
637 if command.contains('/') {
638 let path = Path::new(command);
639 return path.is_file().then(|| path.to_path_buf());
640 }
641 std::env::join_paths(search_dirs)
642 .ok()
643 .and_then(|dirs| {
644 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
645 which::which_in(command, Some(dirs), cwd).ok()
646 })
647 .or_else(|| which::which(command).ok())
648}
649
650pub fn adapter_search_dirs(adapter: &AdapterDescriptor, home: &Path) -> Vec<PathBuf> {
652 adapter
653 .search_dirs
654 .iter()
655 .chain(USER_BIN_DIRS)
656 .map(|dir| home.join(dir))
657 .collect()
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663
664 #[test]
665 fn descriptors_are_unique_and_self_consistent() {
666 let mut names: Vec<_> = ADAPTERS.iter().map(|adapter| adapter.name).collect();
667 names.sort_unstable();
668 names.dedup();
669 assert_eq!(names.len(), ADAPTERS.len());
670 for adapter in ADAPTERS {
671 assert!(
672 adapter.model_args.is_empty()
673 || adapter.model_args.iter().any(|arg| arg.contains("{model}")),
674 "{}",
675 adapter.name
676 );
677 assert!(
678 adapter.effort_args.is_empty()
679 || adapter
680 .effort_args
681 .iter()
682 .any(|arg| arg.contains("{effort}")),
683 "{}",
684 adapter.name
685 );
686 if let Resume::Supported {
687 start,
688 subcommand,
689 options,
690 positional,
691 } = adapter.resume
692 {
693 let names_session =
694 |args: &[&str]| args.iter().any(|arg| arg.contains("{session}"));
695 assert!(start.is_empty() || names_session(start), "{}", adapter.name);
696 assert!(
697 names_session(options) || names_session(positional),
698 "{}",
699 adapter.name
700 );
701 assert!(!names_session(subcommand), "{}", adapter.name);
702 assert!(adapter.conversation_files.is_some(), "{}", adapter.name);
703 }
704 for (variable, _) in adapter
706 .home_environment
707 .iter()
708 .chain(adapter.fixed_environment)
709 {
710 assert!(!variable.ends_with("_API_KEY"), "{variable}");
711 }
712 for store in [
714 match adapter.status {
715 Status::Stored(store) => Some(store),
716 Status::Command(_) => None,
717 },
718 match adapter.logout {
719 Logout::Stored(store) => Some(store),
720 Logout::Command(_) => None,
721 },
722 match adapter.login {
723 Login::ApiKey(store) => Some(store),
724 _ => None,
725 },
726 ]
727 .into_iter()
728 .flatten()
729 {
730 let paths = match store {
731 KeyStore::Grok { auth, config } => vec![auth, config],
732 KeyStore::DshRefs { path, .. } => vec![path],
733 KeyStore::Pi { dir } => vec![dir],
734 KeyStore::Scv { .. } => vec![],
736 };
737 for path in paths {
738 assert!(
739 adapter
740 .home_environment
741 .iter()
742 .any(|(_, home)| !home.is_empty() && path.starts_with(home)),
743 "{}: {path}",
744 adapter.name
745 );
746 }
747 }
748 }
749 }
750
751 #[test]
752 fn status_summaries_never_echo_accounts_or_keys() {
753 let claude = r#"{"loggedIn":true,"authMethod":"claude.ai","email":"me@example.com","orgName":"me@example.com's Organization","subscriptionType":"max"}"#;
754 assert_eq!(
755 summarize_status(StatusSummary::ClaudeJson, true, claude),
756 "signed in (Claude account, max)"
757 );
758 assert_eq!(
759 summarize_status(
760 StatusSummary::ClaudeJson,
761 true,
762 r#"{"loggedIn":true,"authMethod":"api_key","subscriptionType":"me@example.com"}"#
763 ),
764 "signed in (API key)"
765 );
766 assert_eq!(
767 summarize_status(StatusSummary::ClaudeJson, false, r#"{"loggedIn":false}"#),
768 "not signed in"
769 );
770 assert_eq!(
771 summarize_status(
772 StatusSummary::ClaudeJson,
773 true,
774 "{\"loggedIn\":true,\"authMethod\":\"claude.ai\"}\n\nsome stderr"
775 ),
776 "signed in (Claude account)"
777 );
778 assert_eq!(
779 summarize_status(
780 StatusSummary::CodexText,
781 true,
782 "Logged in using an API key - sk-proj-***abcd"
783 ),
784 "signed in (API key)"
785 );
786 assert_eq!(
787 summarize_status(StatusSummary::CodexText, true, "Logged in using ChatGPT"),
788 "signed in (ChatGPT account)"
789 );
790 assert_eq!(
791 summarize_status(StatusSummary::CodexText, false, "Not logged in"),
792 "not signed in"
793 );
794 for adapter in ADAPTERS {
795 if let Status::Command(_) = adapter.status {
796 assert_ne!(
797 adapter.status_summary,
798 StatusSummary::ExitStatus,
799 "{}",
800 adapter.name
801 );
802 }
803 }
804 }
805
806 #[test]
807 fn removal_covers_every_adapter_and_generic_api_keys() {
808 for removed in [
809 "OPENAI_API_KEY",
810 "CLAUDE_CONFIG_DIR",
811 "GROK_HOME",
812 "GROK_AUTH",
813 "XAI_API_KEY",
814 "DSH_HOME",
815 "DSH_PERMISSION_MODE",
816 "DEEPSEEK_BASE_URL",
817 "PI_CODING_AGENT_DIR",
818 "OPENROUTER_API_KEY",
819 "SCV_CONFIG",
820 ] {
821 assert!(is_removed_agent_variable(OsStr::new(removed)), "{removed}");
822 }
823 for kept in ["PATH", "HOME", "LANG", "GH_TOKEN", "GROKKING", "PIPX_HOME"] {
824 assert!(!is_removed_agent_variable(OsStr::new(kept)), "{kept}");
825 }
826 }
827
828 #[test]
829 fn executables_resolve_from_per_user_directories_before_path() {
830 let dir = tempfile::tempdir().unwrap();
831 let bin = dir.path().join(".grok/bin");
832 std::fs::create_dir_all(&bin).unwrap();
833 let name = "scv-test-agent-only-in-home";
834 let executable = bin.join(name);
835 std::fs::write(&executable, "#!/bin/sh\n").unwrap();
836 #[cfg(unix)]
837 {
838 use std::os::unix::fs::PermissionsExt;
839 std::fs::set_permissions(&executable, std::fs::Permissions::from_mode(0o755)).unwrap();
840 }
841 let grok = adapter("grok").unwrap();
842 let dirs = adapter_search_dirs(grok, dir.path());
843 assert!(dirs.contains(&dir.path().join(".local/bin")));
844 assert_eq!(
845 resolve_agent_executable(name, &dirs),
846 Some(executable.clone())
847 );
848 assert_eq!(resolve_agent_executable(name, &[]), None);
849 let shadow = bin.join("sh");
851 std::fs::write(&shadow, "#!/bin/sh\n").unwrap();
852 #[cfg(unix)]
853 {
854 use std::os::unix::fs::PermissionsExt;
855 std::fs::set_permissions(&shadow, std::fs::Permissions::from_mode(0o755)).unwrap();
856 }
857 assert_eq!(resolve_agent_executable("sh", &dirs), Some(shadow));
858 assert!(resolve_agent_executable("sh", &[]).is_some());
859 assert_eq!(
860 resolve_agent_executable(executable.to_str().unwrap(), &[]),
861 Some(executable)
862 );
863 }
864}