From 98f0ef2daf76614b4f36ddf8dfb2c3857125bfc6 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Wed, 22 Jul 2026 19:42:24 -0400 Subject: [PATCH 1/2] fix(hermes): use native Windows trusted recipe --- crates/coven-cli/src/harness.rs | 173 +++++++++++++++++++++++++++++--- 1 file changed, 160 insertions(+), 13 deletions(-) diff --git a/crates/coven-cli/src/harness.rs b/crates/coven-cli/src/harness.rs index ca61a50..764fe0a 100644 --- a/crates/coven-cli/src/harness.rs +++ b/crates/coven-cli/src/harness.rs @@ -900,10 +900,13 @@ pub fn trusted_adapter_manifest_matches_recipe(path: &Path, adapter_id: &str) -> if !metadata.file_type().is_file() { return false; } - if metadata.len() != expected.len() as u64 { - return false; - } - fs::read_to_string(path).is_ok_and(|actual| actual == expected) + fs::read_to_string(path).is_ok_and(|actual| { + actual == expected + // `coven adapter install hermes` wrote this pre-registry recipe. + // Keep recognizing that exact historical document, but execute the + // current platform recipe instead of trusting arbitrary edits. + || (adapter_id == "hermes" && actual == LEGACY_HERMES_ADAPTER_MANIFEST) + }) } fn coven_home_from_process_env() -> Option { @@ -929,14 +932,19 @@ fn adapter_manifest_paths_in_dir(dir: &Path) -> Vec { paths } -pub fn known_adapter_manifest(adapter_id: &str) -> Option<&'static str> { +fn known_adapter_manifest_for_platform(adapter_id: &str, is_windows: bool) -> Option<&'static str> { match adapter_id { "grok" => Some(GROK_BUILD_ADAPTER_MANIFEST), - "hermes" => Some(HERMES_ADAPTER_MANIFEST), + "hermes" if is_windows => Some(HERMES_WINDOWS_ADAPTER_MANIFEST), + "hermes" => Some(HERMES_POSIX_ADAPTER_MANIFEST), _ => None, } } +pub fn known_adapter_manifest(adapter_id: &str) -> Option<&'static str> { + known_adapter_manifest_for_platform(adapter_id, cfg!(windows)) +} + // Grok Build's CLI is documented at https://docs.x.ai/build/cli/reference. // Coven runs it through its `--output-format plain` headless mode (Grok's // own default): per Grok Build's public source, that mode's stdout carries @@ -994,7 +1002,7 @@ const GROK_BUILD_ADAPTER_MANIFEST: &str = r#"{ } "#; -const HERMES_ADAPTER_MANIFEST: &str = r#"{ +const LEGACY_HERMES_ADAPTER_MANIFEST: &str = r#"{ "adapters": [ { "id": "hermes", @@ -1010,6 +1018,78 @@ const HERMES_ADAPTER_MANIFEST: &str = r#"{ } "#; +// This is the current coven-runtimes recipe. Keep accepting the legacy +// manifest above so existing `coven adapter install hermes` installs continue +// to work after upgrading; trusted sources always execute this current recipe. +const HERMES_POSIX_ADAPTER_MANIFEST: &str = r#"{ + "adapters": [ + { + "id": "hermes", + "label": "Hermes Agent", + "executable": "hermes-coven", + "interactive_prompt_prefix_args": [ + "chat", + "--source", + "coven" + ], + "non_interactive_prompt_prefix_args": [ + "chat", + "--source", + "coven", + "-Q" + ], + "install_hint": "Install Hermes Agent, add it to PATH, install the hermes-coven shim, and complete Hermes setup before using this adapter.", + "model_flag": "--model", + "capabilities": { + "stream": false, + "preassigned_session_id": false, + "think": false, + "speed": false + }, + "version": "1.0.2", + "description": "Hermes adapter with native model forwarding. Uses the hermes-coven shim so the harness trailing positional prompt is remapped to hermes chat -q/--query without changing model arguments." + } + ] +} +"#; + +// Hermes ships as a native executable on Windows; the POSIX `hermes-coven` +// shell shim cannot launch there. The prompt flags bind untrusted text as one +// argv value rather than feeding it through cmd.exe parsing. +const HERMES_WINDOWS_ADAPTER_MANIFEST: &str = r#"{ + "adapters": [ + { + "id": "hermes", + "label": "Hermes Agent", + "executable": "hermes", + "interactive_prompt_prefix_args": [ + "chat", + "--source", + "coven" + ], + "non_interactive_prompt_prefix_args": [ + "chat", + "--source", + "coven", + "-Q" + ], + "install_hint": "Install Hermes Agent, add it to PATH, and complete Hermes setup before using this adapter.", + "model_flag": "--model", + "capabilities": { + "stream": false, + "preassigned_session_id": false, + "think": false, + "speed": false + }, + "version": "1.0.2", + "description": "Hermes adapter with native model forwarding and prompt-flag routing for Windows.", + "prompt_flag": "-q", + "interactive_prompt_flag": "-q" + } + ] +} +"#; + pub fn known_adapter_recipe_names() -> &'static [&'static str] { &["grok", "hermes"] } @@ -3156,7 +3236,10 @@ mod tests { let coven_home = temp_dir.path().join("coven-home"); let adapter_dir = coven_home.join("adapters"); fs::create_dir_all(&adapter_dir)?; - fs::write(adapter_dir.join("hermes.json"), HERMES_ADAPTER_MANIFEST)?; + fs::write( + adapter_dir.join("hermes.json"), + HERMES_POSIX_ADAPTER_MANIFEST, + )?; let _guard = lock_env(); let _manifest_guard = EnvVarGuard::remove(EXTERNAL_ADAPTER_MANIFEST_ENV); @@ -3210,7 +3293,10 @@ mod tests { let coven_home = temp_dir.path().join("coven-home"); let adapter_dir = coven_home.join("adapters"); fs::create_dir_all(&adapter_dir)?; - fs::write(adapter_dir.join("hermes.json"), HERMES_ADAPTER_MANIFEST)?; + fs::write( + adapter_dir.join("hermes.json"), + HERMES_POSIX_ADAPTER_MANIFEST, + )?; let _guard = lock_env(); let _manifest_guard = EnvVarGuard::remove(EXTERNAL_ADAPTER_MANIFEST_ENV); @@ -3232,14 +3318,75 @@ mod tests { } #[test] - fn trusted_adapter_manifest_rejects_size_mismatches_before_content_match() -> anyhow::Result<()> - { + fn hermes_trusted_recipe_uses_native_prompt_flags_on_windows() -> anyhow::Result<()> { + let built_ins = built_in_harness_specs(); + let windows = parse_external_harness_specs( + known_adapter_manifest_for_platform("hermes", true).expect("Windows recipe"), + Path::new("hermes.json"), + &built_ins, + )?; + let windows = windows + .into_iter() + .find(|spec| spec.id == "hermes") + .expect("Windows Hermes spec"); + assert_eq!(windows.executable, "hermes"); + assert_eq!(windows.prompt_flag.as_deref(), Some("-q")); + assert_eq!(windows.interactive_prompt_flag.as_deref(), Some("-q")); + + let posix = parse_external_harness_specs( + known_adapter_manifest_for_platform("hermes", false).expect("POSIX recipe"), + Path::new("hermes.json"), + &built_ins, + )?; + let posix = posix + .into_iter() + .find(|spec| spec.id == "hermes") + .expect("POSIX Hermes spec"); + assert_eq!(posix.executable, "hermes-coven"); + assert_eq!(posix.prompt_flag, None); + assert_eq!(posix.interactive_prompt_flag, None); + Ok(()) + } + + #[test] + fn trusted_legacy_hermes_manifest_upgrades_to_the_current_recipe() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir()?; + let adapters_dir = temp_dir.path().join("adapters"); + fs::create_dir_all(&adapters_dir)?; + let manifest_path = adapters_dir.join("hermes.json"); + fs::write(&manifest_path, LEGACY_HERMES_ADAPTER_MANIFEST)?; + + assert!(trusted_adapter_manifest_matches_recipe( + &manifest_path, + "hermes" + )); + let mut sources = trusted_adapter_manifest_sources(temp_dir.path()); + assert_eq!(sources.len(), 1); + let hermes = sources + .remove(0) + .load_specs(&built_in_harness_specs())? + .into_iter() + .find(|spec| spec.id == "hermes") + .expect("current trusted Hermes recipe"); + assert_eq!( + hermes.executable, + if cfg!(windows) { + "hermes" + } else { + "hermes-coven" + } + ); + Ok(()) + } + + #[test] + fn trusted_adapter_manifest_rejects_noncanonical_content() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir()?; let coven_home = temp_dir.path().join("coven-home"); let adapter_dir = coven_home.join("adapters"); fs::create_dir_all(&adapter_dir)?; let manifest_path = adapter_dir.join("hermes.json"); - fs::write(&manifest_path, format!("{HERMES_ADAPTER_MANIFEST}\n"))?; + fs::write(&manifest_path, format!("{HERMES_POSIX_ADAPTER_MANIFEST}\n"))?; assert!(!trusted_adapter_manifest_matches_recipe( &manifest_path, @@ -3256,7 +3403,7 @@ mod tests { let adapter_dir = coven_home.join("adapters"); fs::create_dir_all(&adapter_dir)?; let manifest_path = adapter_dir.join("hermes.json"); - fs::write(&manifest_path, HERMES_ADAPTER_MANIFEST)?; + fs::write(&manifest_path, HERMES_POSIX_ADAPTER_MANIFEST)?; let mut sources = trusted_adapter_manifest_sources(&coven_home); assert_eq!(sources.len(), 1); From e66c2f83410eea3bda13f3d1c86690c51ca40de0 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Wed, 22 Jul 2026 20:05:34 -0400 Subject: [PATCH 2/2] test(hermes): cover trusted recipe on Windows --- crates/coven-cli/src/harness.rs | 67 ++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/crates/coven-cli/src/harness.rs b/crates/coven-cli/src/harness.rs index 764fe0a..d3fc9c7 100644 --- a/crates/coven-cli/src/harness.rs +++ b/crates/coven-cli/src/harness.rs @@ -3231,14 +3231,14 @@ mod tests { } #[test] - fn hermes_recipe_forwards_selected_models_before_the_shim_prompt() -> anyhow::Result<()> { + fn hermes_recipe_forwards_selected_models_and_prompts() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir()?; let coven_home = temp_dir.path().join("coven-home"); let adapter_dir = coven_home.join("adapters"); fs::create_dir_all(&adapter_dir)?; fs::write( adapter_dir.join("hermes.json"), - HERMES_POSIX_ADAPTER_MANIFEST, + known_adapter_manifest("hermes").expect("current Hermes recipe"), )?; let _guard = lock_env(); @@ -3257,19 +3257,38 @@ mod tests { ..Default::default() }, )?; - assert_eq!(selected.0, "hermes-coven"); + assert_eq!( + selected.0, + if cfg!(windows) { + "hermes" + } else { + "hermes-coven" + } + ); assert_eq!( selected.1, - vec![ - "--model", - "gpt-5.6-terra", - "chat", - "--source", - "coven", - "-Q", - "--", - "hello from Coven", - ] + if cfg!(windows) { + vec![ + "--model", + "gpt-5.6-terra", + "chat", + "--source", + "coven", + "-Q", + "-q=hello from Coven", + ] + } else { + vec![ + "--model", + "gpt-5.6-terra", + "chat", + "--source", + "coven", + "-Q", + "--", + "hello from Coven", + ] + } ); let inherited = command_parts_for_harness_with_conversation( @@ -3282,7 +3301,11 @@ mod tests { )?; assert_eq!( inherited.1, - vec!["chat", "--source", "coven", "-Q", "--", "hello from Coven"] + if cfg!(windows) { + vec!["chat", "--source", "coven", "-Q", "-q=hello from Coven"] + } else { + vec!["chat", "--source", "coven", "-Q", "--", "hello from Coven"] + } ); Ok(()) } @@ -3295,7 +3318,7 @@ mod tests { fs::create_dir_all(&adapter_dir)?; fs::write( adapter_dir.join("hermes.json"), - HERMES_POSIX_ADAPTER_MANIFEST, + known_adapter_manifest("hermes").expect("current Hermes recipe"), )?; let _guard = lock_env(); @@ -3403,7 +3426,10 @@ mod tests { let adapter_dir = coven_home.join("adapters"); fs::create_dir_all(&adapter_dir)?; let manifest_path = adapter_dir.join("hermes.json"); - fs::write(&manifest_path, HERMES_POSIX_ADAPTER_MANIFEST)?; + fs::write( + &manifest_path, + known_adapter_manifest("hermes").expect("current Hermes recipe"), + )?; let mut sources = trusted_adapter_manifest_sources(&coven_home); assert_eq!(sources.len(), 1); @@ -3418,7 +3444,14 @@ mod tests { .find(|spec| spec.id == "hermes") .expect("trusted source should parse bundled hermes recipe"); - assert_eq!(hermes.executable, "hermes-coven"); + assert_eq!( + hermes.executable, + if cfg!(windows) { + "hermes" + } else { + "hermes-coven" + } + ); assert_eq!( hermes.manifest_path.as_deref(), Some(manifest_path.to_string_lossy().as_ref())