Refuse a folder or app bundle as an agent's program

The "Where is the program?" picker returned Antigravity.app on a Mac, and the path was saved
as the agent's command (#467). set_command now refuses a folder for a local agent, with a
message that names the command-line program to pick. A folder saved before that is not
started: the usual program name is used, as when none is chosen.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
Fábio Soares
2026-11-04 03:11:44 +00:00
parent c7efeb99db
commit 77495e354e
4 changed files with 48 additions and 4 deletions
+11
View File
@@ -64,6 +64,11 @@ pub fn program_of(config: &BackendConfig, options: &BackendOptions) -> String {
let usual = program_for(&config.model);
let Some(chosen) = options.command.as_deref().map(str::trim).filter(|c| !c.is_empty()) else { return usual };
let local = config.ssh_host.as_deref().is_none_or(|h| h.trim().is_empty());
// A folder or app bundle saved before set_command refused it (#467) can't be started: the usual
// program is used, as when none is chosen.
if local && Path::new(chosen).is_dir() {
return usual;
}
let path = Path::new(chosen);
if !local || path.parent().is_none_or(|p| p.as_os_str().is_empty()) || path.exists() {
return chosen.to_string();
@@ -1118,6 +1123,12 @@ mod tests {
assert_eq!(program_of(&config(None), &chosen(&new)), new.display().to_string(), "a path that's there is used as it is");
assert_eq!(program_of(&config(Some("box")), &chosen(&gone)), gone.display().to_string(), "another computer's path isn't checked here");
assert_eq!(program_of(&config(None), &BackendOptions::default()), "codex", "nothing chosen: the usual name");
// #467: a saved app bundle is never started; the usual name is looked up instead.
let dir = tempfile::tempdir().unwrap();
let bundle = dir.path().join("Codex.app");
std::fs::create_dir_all(&bundle).unwrap();
assert_eq!(program_of(&config(None), &chosen(&bundle)), "codex", "an app bundle is not the program");
assert_eq!(program_of(&config(Some("box")), &chosen(&bundle)), bundle.display().to_string(), "another computer's path isn't checked here");
let bare = BackendOptions { command: Some("my-codex".into()), ..Default::default() };
assert_eq!(program_of(&config(None), &bare), "my-codex", "a bare name is left to PATH");
}
+12 -1
View File
@@ -522,9 +522,20 @@ impl AiHub {
/// The program an agent app runs (`None` or empty: found by its usual name, on PATH and in the
/// usual install folders). Returns false when there was no such backend.
pub fn set_command(&self, name: &str, command: Option<&str>) -> Result<bool, ToolError> {
let command = command.map(str::trim).filter(|c| !c.is_empty());
let mut cfg = self.config_to_change()?;
let Some(b) = cfg.backends.iter_mut().find(|b| b.config.name == name) else { return Ok(false) };
b.options.command = command.map(str::trim).filter(|c| !c.is_empty()).map(str::to_string);
// A folder can't be started, and on a Mac an app is one (#467: the file picker returned
// `Antigravity.app`). Another computer's path isn't looked at here.
let local = b.config.ssh_host.as_deref().is_none_or(|h| h.trim().is_empty());
if let Some(c) = command
&& local
&& std::path::Path::new(c).is_dir()
{
return Err(ToolError::invalid(format!("{c} is a folder, so it can't be the agent's program. On a Mac an app is a folder."))
.with_remediation("Pick the agent's command-line program instead, such as `agy`, or clear the program to use the usual one."));
}
b.options.command = command.map(str::to_string);
self.save_config(&cfg)?;
Ok(true)
}
+21
View File
@@ -304,6 +304,27 @@ fn an_agent_apps_program_is_set_and_cleared() {
assert_eq!(program(&h), None, "blank finds it by its usual name again");
}
/// #467: a folder or app bundle can't be an agent's program (the file picker returns
/// `Antigravity.app` on a Mac); it's refused and the saved program stays as it was.
#[test]
fn an_app_bundle_is_refused_as_an_agents_program() {
let (h, d) = hub();
add(&h, "OpenCode", BackendKind::AgentCli, "http://127.0.0.1:9", None);
let program = |h: &AiHub| h.options().get("OpenCode").and_then(|o| o.command.clone());
let bundle = d.path().join("Antigravity.app");
std::fs::create_dir_all(bundle.join("Contents")).unwrap();
let err = h.set_command("OpenCode", Some(&bundle.display().to_string())).unwrap_err();
assert_eq!(err.code, ErrorCode::BadInput, "{err:?}");
assert!(err.message.contains("is a folder"), "{}", err.message);
assert_eq!(program(&h), None, "nothing is saved for a refused folder");
// A file, and a bare name, are still accepted.
let file = d.path().join("agy");
std::fs::write(&file, "#!/bin/sh\n").unwrap();
assert!(h.set_command("OpenCode", Some(&file.display().to_string())).unwrap());
assert!(h.set_command("OpenCode", Some("agy")).unwrap());
assert_eq!(program(&h).as_deref(), Some("agy"));
}
/// Claude Code's folder (`CLAUDE_CONFIG_DIR`) is chosen per AI, so two can be two accounts; only
/// a full path, and only for an agent app that keeps one.
#[test]
+4 -3
View File
@@ -543,10 +543,11 @@ Owner: open · Status: in progress
and add search paths only for a new report that has that output
- [ ] Say plainly when what's found is the Claude desktop app rather than the `claude` program
(#259, a guess at its cause)
- [ ] macOS: `/Applications/Antigravity.app` ended up as the agent's command (#467). Found: the "Where is
- [x] macOS: `/Applications/Antigravity.app` ended up as the agent's command (#467). Found: the "Where is
the program?" picker (`pick_file`) returns the bundle, and `set_command` saved any path. Refuse a
folder or app bundle when it's saved, and don't run one that was saved before — claimed: fabox-os,
branch bugfix/467-agent-program-app-bundle
folder or app bundle when it's saved (`set_command`), and don't run one that was saved before
(`program_of` uses the usual name). Tests: `an_app_bundle_is_refused_as_an_agents_program`, `program_of`
cases. — claimed: fabox-os, branch bugfix/467-agent-program-app-bundle
- [x] Windows: look in `%LOCALAPPDATA%\agy\bin` too (#62) — claimed: Afloat16, branch bugfix/r2.2-antigravity-search-20261103
- [x] Windows: `usual_windows_dirs_come_after_path` counts Antigravity's second folder (#748
added it; the test runs only on Windows, which CI doesn't)