-
Notifications
You must be signed in to change notification settings - Fork 52
feat(cli): validate duplicate agent executables during dry run #736
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c284414
79760d0
49509aa
ba3bb8a
7818ea5
6c7387e
5a921e0
01e538a
36ea901
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,9 +11,14 @@ use super::serve::ServerArgs; | |
| use crate::agents::CodingAgent; | ||
| use crate::error::CliError; | ||
|
|
||
| const POSSIBLE_DUPLICATE_AGENT_EXECUTABLE: &str = "possible_duplicate_agent_executable"; | ||
|
|
||
| /// Args for an easy-path agent shortcut. | ||
| #[derive(Debug, Clone, Args)] | ||
| pub(crate) struct EasyPathCommand { | ||
| /// Print the resolved launch plan, including forwarded arguments, without executing it. | ||
| #[arg(long)] | ||
| pub(super) dry_run: bool, | ||
| #[arg(last = true)] | ||
| pub(super) command: Vec<String>, | ||
| } | ||
|
|
@@ -60,7 +65,13 @@ pub(super) async fn execute( | |
| command: RunCommand, | ||
| server: &ServerArgs, | ||
| ) -> Result<ExitCode, CliError> { | ||
| if command.dry_run | ||
| && let Some(agent) = command.agent.map(Into::into) | ||
| { | ||
| warn_for_possible_duplicate(agent, &command.command); | ||
| } | ||
|
Comment on lines
+68
to
+72
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic doesn't make sense because it falls through. If
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The call does not fall through into live execution. |
||
| let inherited = server.to_runtime(); | ||
| // The launcher prints the plan and returns before gateway or child execution for dry runs. | ||
| crate::process::launcher::run(command.into_runtime(), Some(&inherited)).await | ||
| } | ||
|
|
||
|
|
@@ -79,13 +90,16 @@ pub(super) async fn easy_path( | |
| command: EasyPathCommand, | ||
| server: &ServerArgs, | ||
| ) -> Result<ExitCode, CliError> { | ||
| if command.dry_run { | ||
| warn_for_possible_duplicate(agent, &command.command); | ||
| } | ||
| let inherited = server.to_runtime(); | ||
| // An explicit config path is the user's contract. Without one, setup is required only when | ||
| // none of the normal discovery layers exists. Keep this interactive decision in the command | ||
| // layer so process supervision receives a complete, agent-neutral run request. | ||
| let explicit_config = inherited.config.as_deref(); | ||
| let needs_setup = explicit_config.is_none() && !crate::configuration::any_config_file_exists(); | ||
| if needs_setup { | ||
| if needs_setup && !command.dry_run { | ||
| let explicit_plugin_path = easy_path_plugin_config_path(&inherited); | ||
| super::configure::run(Some(agent), explicit_plugin_path).await?; | ||
| } | ||
|
|
@@ -96,9 +110,35 @@ pub(super) async fn easy_path( | |
| anthropic_base_url: None, | ||
| session_metadata: None, | ||
| plugin_config_path: None, | ||
| dry_run: false, | ||
| dry_run: command.dry_run, | ||
| print: false, | ||
| command: command.command, | ||
| }; | ||
| // The launcher prints the plan and returns before gateway or child execution for dry runs. | ||
| crate::process::launcher::run(runtime, Some(&inherited)).await | ||
| } | ||
|
|
||
| fn warn_for_possible_duplicate(agent: CodingAgent, command: &[String]) { | ||
| if !has_duplicate_agent_executable(agent, command) { | ||
| return; | ||
| } | ||
| let agent = agent.as_arg(); | ||
| log::warn!( | ||
| target: "nemo_relay.cli", | ||
| event = "agent_invocation_warning", | ||
| diagnostic_code = POSSIBLE_DUPLICATE_AGENT_EXECUTABLE, | ||
| agent = agent, | ||
| duplicate_executable = agent, | ||
| confidence = "high", | ||
| action = "remove_duplicate_executable", | ||
| command_modified = false, | ||
| arguments_redacted = true; | ||
| "Possible duplicate agent executable after `--`; remove the repeated executable" | ||
| ); | ||
| } | ||
|
|
||
| pub(super) fn has_duplicate_agent_executable(agent: CodingAgent, command: &[String]) -> bool { | ||
| command | ||
| .first() | ||
| .is_some_and(|executable| CodingAgent::infer(executable) == Some(agent)) | ||
| } | ||
|
Comment on lines
+140
to
+144
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This internal detail should be inline to |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -345,6 +345,68 @@ fn doctor_accepts_offline_flag() { | |
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn agent_shortcut_parser_accepts_dry_run_before_forwarded_arguments() { | ||
| for shortcut in ["claude", "codex", "hermes"] { | ||
| let cli = Cli::try_parse_from([ | ||
| "nemo-relay", | ||
| shortcut, | ||
| "--dry-run", | ||
| "--", | ||
| shortcut, | ||
| "synthetic argument", | ||
| ]) | ||
| .unwrap(); | ||
| let command = match cli.command { | ||
| Some(Command::Claude(command)) | ||
| | Some(Command::Codex(command)) | ||
| | Some(Command::Hermes(command)) => command, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hermes agent has been removed |
||
| other => panic!("expected agent shortcut command, got {other:?}"), | ||
| }; | ||
| assert!(command.dry_run); | ||
| assert_eq!(command.command, [shortcut, "synthetic argument"]); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn dry_run_diagnostic_recognizes_supported_agent_executable_forms() { | ||
| let cases = [ | ||
| (CodingAgent::ClaudeCode, "claude"), | ||
| (CodingAgent::ClaudeCode, "claude-code"), | ||
| (CodingAgent::ClaudeCode, "/opt/bin/claude"), | ||
| (CodingAgent::ClaudeCode, "/opt/bin/claude-code.exe"), | ||
| (CodingAgent::Codex, "codex"), | ||
| (CodingAgent::Codex, r"C:\tools\CODEX.CMD"), | ||
| (CodingAgent::Codex, r"C:\tools\codex.com"), | ||
| (CodingAgent::Hermes, "hermes"), | ||
| (CodingAgent::Hermes, "hermes-agent"), | ||
| (CodingAgent::Hermes, "/opt/bin/hermes-agent.bat"), | ||
|
Comment on lines
+381
to
+383
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No longer exists |
||
| ]; | ||
|
|
||
| for (agent, executable) in cases { | ||
| let command = vec![executable.to_string(), "synthetic argument".to_string()]; | ||
| assert!( | ||
| run::has_duplicate_agent_executable(agent, &command), | ||
| "expected {executable:?} to duplicate {agent:?}" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn dry_run_diagnostic_checks_only_the_first_forwarded_token() { | ||
| for command in [ | ||
| vec![], | ||
| vec!["-p".to_string(), "claude appears later".to_string()], | ||
| vec!["my-wrapper".to_string(), "claude".to_string()], | ||
| vec!["codex".to_string(), "claude".to_string()], | ||
| ] { | ||
| assert!( | ||
| !run::has_duplicate_agent_executable(CodingAgent::ClaudeCode, &command), | ||
| "unexpected duplicate for {command:?}" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn multi_agent_operations_attempt_every_target_before_reporting_errors() { | ||
| let visited = std::cell::RefCell::new(Vec::new()); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
only used in one place. no need for constant