2 Commits

Author SHA1 Message Date
github-actions[bot] 0c24694ff5 chore: bump Cargo.toml to 0.7.0 2026-06-18 22:01:38 +00:00
github-actions[bot] 1e006256f1 bump: version 0.6.0 → 0.7.0 [skip ci] 2026-06-18 22:01:35 +00:00
7 changed files with 18 additions and 300 deletions
-7
View File
@@ -1,14 +1,7 @@
## v0.7.1 (2026-06-19)
### Fix
- sbx mixins must be passed in directories, not as files and the files must be named spec.yaml per new sbx version
## v0.7.0 (2026-06-18) ## v0.7.0 (2026-06-18)
### Feat ### Feat
- added configurable cache path via the COYOTE_CACHE_PATH environment variable
- added a memory option to .set tab completions - added a memory option to .set tab completions
- Added a diagnostic .info tools subcommand to make it easier to see what tools are enabled in all contexts - Added a diagnostic .info tools subcommand to make it easier to see what tools are enabled in all contexts
- Added additional info outputs for enabled skills and sbx directories - Added additional info outputs for enabled skills and sbx directories
Generated
+5 -5
View File
@@ -1060,9 +1060,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.65" version = "1.2.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -1402,7 +1402,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "coyote-ai" name = "coyote-ai"
version = "0.7.1" version = "0.7.0"
dependencies = [ dependencies = [
"ansi_colours", "ansi_colours",
"anyhow", "anyhow",
@@ -6712,9 +6712,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols" name = "wayland-protocols"
version = "0.32.13" version = "0.32.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"wayland-backend", "wayland-backend",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "coyote-ai" name = "coyote-ai"
version = "0.7.1" version = "0.7.0"
edition = "2024" edition = "2024"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"] authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "An all-in-one, batteries included LLM CLI Tool" description = "An all-in-one, batteries included LLM CLI Tool"
-1
View File
@@ -147,7 +147,6 @@ const SBX_KIT_DIR_NAME: &str = "sbx-kit";
const SBX_KIT_HASH_FILE: &str = "kit.sha256"; const SBX_KIT_HASH_FILE: &str = "kit.sha256";
const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml"; const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml";
const SBX_VAULT_MIXINS_DIR_NAME: &str = "sbx-vault-mixins"; const SBX_VAULT_MIXINS_DIR_NAME: &str = "sbx-vault-mixins";
const SBX_MIXIN_KITS_DIR_NAME: &str = "sbx-mixin-kits";
const GIT_DIR_NAME: &str = ".git"; const GIT_DIR_NAME: &str = ".git";
const GITIGNORE_FILE_NAME: &str = ".gitignore"; const GITIGNORE_FILE_NAME: &str = ".gitignore";
const DEFAULT_VISIBLE_TOOLS: [&str; 18] = [ const DEFAULT_VISIBLE_TOOLS: [&str; 18] = [
+4 -14
View File
@@ -4,8 +4,8 @@ use super::{
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME, ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, MEMORY_DIR_NAME, GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, MEMORY_DIR_NAME,
MEMORY_INDEX_FILE_NAME, ModelsOverride, RAGS_DIR_NAME, ROLES_DIR_NAME, SBX_KIT_DIR_NAME, MEMORY_INDEX_FILE_NAME, ModelsOverride, RAGS_DIR_NAME, ROLES_DIR_NAME, SBX_KIT_DIR_NAME,
SBX_KIT_HASH_FILE, SBX_MIXIN_FILE_NAME, SBX_MIXIN_KITS_DIR_NAME, SBX_VAULT_MIXINS_DIR_NAME, SBX_KIT_HASH_FILE, SBX_MIXIN_FILE_NAME, SBX_VAULT_MIXINS_DIR_NAME, SKILLS_DIR_NAME,
SKILLS_DIR_NAME, WORKSPACE_MEMORY_DIR_NAME, WORKSPACE_MEMORY_DIR_NAME,
}; };
use crate::client::ProviderModels; use crate::client::ProviderModels;
use crate::utils::{get_env_name, list_file_names, normalize_env_name}; use crate::utils::{get_env_name, list_file_names, normalize_env_name};
@@ -33,14 +33,8 @@ pub fn local_path(name: &str) -> PathBuf {
} }
pub fn cache_path() -> PathBuf { pub fn cache_path() -> PathBuf {
if let Ok(v) = env::var(get_env_name("cache_dir")) { let base_dir = dirs::cache_dir().unwrap_or_else(env::temp_dir);
PathBuf::from(v) base_dir.join(env!("CARGO_CRATE_NAME"))
} else if let Ok(v) = env::var("XDG_CACHE_HOME") {
PathBuf::from(v).join(env!("CARGO_CRATE_NAME"))
} else {
let base_dir = dirs::cache_dir().unwrap_or_else(env::temp_dir);
base_dir.join(env!("CARGO_CRATE_NAME"))
}
} }
pub fn sandbox_kit_override() -> Option<PathBuf> { pub fn sandbox_kit_override() -> Option<PathBuf> {
@@ -154,10 +148,6 @@ pub fn sbx_vault_mixins_hash_file() -> PathBuf {
sbx_vault_mixins_dir().join(SBX_KIT_HASH_FILE) sbx_vault_mixins_dir().join(SBX_KIT_HASH_FILE)
} }
pub fn sbx_mixin_kits_dir() -> PathBuf {
cache_path().join(SBX_MIXIN_KITS_DIR_NAME)
}
pub fn config_file() -> PathBuf { pub fn config_file() -> PathBuf {
match env::var(get_env_name("config_file")) { match env::var(get_env_name("config_file")) {
Ok(value) => PathBuf::from(value), Ok(value) => PathBuf::from(value),
-205
View File
@@ -1,16 +1,13 @@
use std::env; use std::env;
use std::fs;
use std::fs::{read_dir, read_to_string}; use std::fs::{read_dir, read_to_string};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde_yaml::Value; use serde_yaml::Value;
use sha2::{Digest, Sha256};
use crate::config::paths; use crate::config::paths;
const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml"; const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml";
const KIT_SPEC_FILE_NAME: &str = "spec.yaml";
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DiscoveredMixin { pub struct DiscoveredMixin {
@@ -20,46 +17,6 @@ pub struct DiscoveredMixin {
pub domain_count: usize, pub domain_count: usize,
} }
impl DiscoveredMixin {
pub fn kit_path(&self) -> Result<PathBuf> {
if self.path.is_dir() {
return Ok(self.path.clone());
}
wrap_mixin_as_kit(&self.path)
}
}
pub fn wrap_mixin_as_kit(mixin_path: &Path) -> Result<PathBuf> {
let bytes = fs::read(mixin_path)
.with_context(|| format!("Failed to read sbx mixin {}", mixin_path.display()))?;
let mut hasher = Sha256::new();
hasher.update(&bytes);
let hash = format!("{:x}", hasher.finalize());
let kit_dir = paths::sbx_mixin_kits_dir().join(&hash);
let spec_path = kit_dir.join(KIT_SPEC_FILE_NAME);
if let Ok(existing) = fs::read(&spec_path)
&& existing == bytes
{
return Ok(kit_dir);
}
fs::create_dir_all(&kit_dir)
.with_context(|| format!("Failed to create mixin kit dir {}", kit_dir.display()))?;
fs::write(&spec_path, &bytes)
.with_context(|| format!("Failed to write {}", spec_path.display()))?;
debug!(
"Wrapped mixin {} as kit at {}",
mixin_path.display(),
kit_dir.display()
);
Ok(kit_dir)
}
pub fn discover() -> Result<Vec<DiscoveredMixin>> { pub fn discover() -> Result<Vec<DiscoveredMixin>> {
let mut out = Vec::new(); let mut out = Vec::new();
@@ -277,166 +234,4 @@ network:
let found = collect_subdir_mixins(&absent); let found = collect_subdir_mixins(&absent);
assert!(found.is_empty()); assert!(found.is_empty());
} }
mod wrap_as_kit {
use super::*;
use serial_test::serial;
use std::ffi::OsString;
struct TestCacheDirGuard {
key: String,
previous: Option<OsString>,
path: PathBuf,
}
impl TestCacheDirGuard {
fn new() -> Self {
let key = crate::utils::get_env_name("cache_dir");
let previous = env::var_os(&key);
let nanos = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = env::temp_dir().join(format!("coyote-mixin-wrap-cache-{nanos}"));
fs::create_dir_all(&path).unwrap();
unsafe {
env::set_var(&key, &path);
}
Self {
key,
previous,
path,
}
}
}
impl Drop for TestCacheDirGuard {
fn drop(&mut self) {
unsafe {
match &self.previous {
Some(v) => env::set_var(&self.key, v),
None => env::remove_var(&self.key),
}
}
let _ = fs::remove_dir_all(&self.path);
}
}
fn write_mixin(name: &str, content: &str) -> PathBuf {
let root = unique_root(&format!("wrap-src-{name}"));
let path = root.join("sbx-mixin.yaml");
fs::write(&path, content).unwrap();
path
}
#[test]
#[serial]
fn wrap_mixin_as_kit_creates_spec_yaml_with_original_content() {
let _guard = TestCacheDirGuard::new();
let content = "schemaVersion: \"1\"\nkind: mixin\nname: probe\n";
let mixin = write_mixin("content", content);
let kit_dir = wrap_mixin_as_kit(&mixin).unwrap();
let spec = kit_dir.join("spec.yaml");
assert!(spec.exists(), "spec.yaml must exist in wrapped kit dir");
assert_eq!(fs::read_to_string(&spec).unwrap(), content);
}
#[test]
#[serial]
fn wrap_mixin_as_kit_is_deterministic_for_identical_content() {
let _guard = TestCacheDirGuard::new();
let content = "schemaVersion: \"1\"\nkind: mixin\nname: probe\n";
let mixin_one = write_mixin("dedup-1", content);
let mixin_two = write_mixin("dedup-2", content);
let kit_a = wrap_mixin_as_kit(&mixin_one).unwrap();
let kit_b = wrap_mixin_as_kit(&mixin_two).unwrap();
assert_eq!(
kit_a, kit_b,
"same content should share the same content-addressed kit dir"
);
}
#[test]
#[serial]
fn wrap_mixin_as_kit_different_content_yields_different_dirs() {
let _guard = TestCacheDirGuard::new();
let mixin_a = write_mixin("diff-a", "kind: mixin\nname: a\n");
let mixin_b = write_mixin("diff-b", "kind: mixin\nname: b\n");
let kit_a = wrap_mixin_as_kit(&mixin_a).unwrap();
let kit_b = wrap_mixin_as_kit(&mixin_b).unwrap();
assert_ne!(
kit_a, kit_b,
"different content must hash to different kit dirs"
);
}
#[test]
#[serial]
fn wrap_mixin_as_kit_is_idempotent_on_cache_hit() {
let _guard = TestCacheDirGuard::new();
let mixin = write_mixin("idempotent", "kind: mixin\nname: probe\n");
let kit_first = wrap_mixin_as_kit(&mixin).unwrap();
let spec = kit_first.join("spec.yaml");
let mtime_first = fs::metadata(&spec).unwrap().modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let kit_second = wrap_mixin_as_kit(&mixin).unwrap();
let mtime_second = fs::metadata(kit_second.join("spec.yaml"))
.unwrap()
.modified()
.unwrap();
assert_eq!(kit_first, kit_second);
assert_eq!(
mtime_first, mtime_second,
"cache hit must not rewrite spec.yaml"
);
}
#[test]
#[serial]
fn kit_path_passes_through_existing_directory() {
let _guard = TestCacheDirGuard::new();
let dir = unique_root("kit-path-dir-passthrough");
let m = DiscoveredMixin {
path: dir.clone(),
label: "vault".into(),
install_count: 1,
domain_count: 1,
};
assert_eq!(m.kit_path().unwrap(), dir);
}
#[test]
#[serial]
fn kit_path_wraps_file_into_kit_dir() {
let _guard = TestCacheDirGuard::new();
let mixin = write_mixin("kit-path-wrap", "kind: mixin\nname: probe\n");
let m = DiscoveredMixin {
path: mixin.clone(),
label: mixin.display().to_string(),
install_count: 0,
domain_count: 0,
};
let wrapped = m.kit_path().unwrap();
assert!(wrapped.is_dir(), "kit_path of a file should be a directory");
assert!(wrapped.join("spec.yaml").exists());
assert_ne!(
wrapped, mixin,
"kit_path should not return the original file path"
);
}
}
} }
+8 -67
View File
@@ -347,13 +347,12 @@ fn build_create_args(
]; ];
for mixin in mixins { for mixin in mixins {
let mixin_kit = mixin.kit_path()?; let mixin_str = mixin
let mixin_str = mixin_kit .path
.to_str() .to_str()
.ok_or_else(|| anyhow!("Mixin kit path is not valid UTF-8: {}", mixin_kit.display()))? .ok_or_else(|| anyhow!("Mixin path is not valid UTF-8: {}", mixin.path.display()))?;
.to_string();
args.push("--kit".to_string()); args.push("--kit".to_string());
args.push(mixin_str); args.push(mixin_str.to_string());
} }
args.push(SANDBOX_AGENT.to_string()); args.push(SANDBOX_AGENT.to_string());
@@ -591,24 +590,15 @@ mod tests {
#[test] #[test]
fn build_create_args_emits_base_kit_before_mixins() { fn build_create_args_emits_base_kit_before_mixins() {
let kit = PathBuf::from("/cache/sbx-kit"); let kit = PathBuf::from("/cache/sbx-kit");
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir_a = env::temp_dir().join(format!("coyote-mixin-a-{unique}"));
let dir_b = env::temp_dir().join(format!("coyote-mixin-b-{unique}"));
fs::create_dir_all(&dir_a).unwrap();
fs::create_dir_all(&dir_b).unwrap();
let mixins = vec![ let mixins = vec![
DiscoveredMixin { DiscoveredMixin {
path: dir_a.clone(), path: PathBuf::from("/cfg/sbx-mixin.yaml"),
label: "user".into(), label: "user".into(),
install_count: 0, install_count: 0,
domain_count: 0, domain_count: 0,
}, },
DiscoveredMixin { DiscoveredMixin {
path: dir_b.clone(), path: PathBuf::from("/cfg/agents/sql/sbx-mixin.yaml"),
label: "sql".into(), label: "sql".into(),
install_count: 0, install_count: 0,
domain_count: 0, domain_count: 0,
@@ -624,18 +614,15 @@ mod tests {
"--kit".to_string(), "--kit".to_string(),
"/cache/sbx-kit".to_string(), "/cache/sbx-kit".to_string(),
"--kit".to_string(), "--kit".to_string(),
dir_a.display().to_string(), "/cfg/sbx-mixin.yaml".to_string(),
"--kit".to_string(), "--kit".to_string(),
dir_b.display().to_string(), "/cfg/agents/sql/sbx-mixin.yaml".to_string(),
"coyote".to_string(), "coyote".to_string(),
"--name".to_string(), "--name".to_string(),
"my-box".to_string(), "my-box".to_string(),
".".to_string(), ".".to_string(),
] ]
); );
let _ = fs::remove_dir_all(&dir_a);
let _ = fs::remove_dir_all(&dir_b);
} }
#[test] #[test]
@@ -658,7 +645,6 @@ mod tests {
mod vault_mixins { mod vault_mixins {
use super::*; use super::*;
use crate::utils::get_env_name;
use gman::providers::aws_secrets_manager::AwsSecretsManagerProvider; use gman::providers::aws_secrets_manager::AwsSecretsManagerProvider;
use gman::providers::azure_key_vault::AzureKeyVaultProvider; use gman::providers::azure_key_vault::AzureKeyVaultProvider;
use gman::providers::gcp_secret_manager::GcpSecretManagerProvider; use gman::providers::gcp_secret_manager::GcpSecretManagerProvider;
@@ -666,46 +652,6 @@ mod tests {
use gman::providers::local::LocalProvider; use gman::providers::local::LocalProvider;
use gman::providers::one_password::OnePasswordProvider; use gman::providers::one_password::OnePasswordProvider;
use serial_test::serial; use serial_test::serial;
use std::time::{SystemTime, UNIX_EPOCH};
struct TestCacheDirGuard {
key: String,
previous: Option<std::ffi::OsString>,
path: PathBuf,
}
impl TestCacheDirGuard {
fn new() -> Self {
let key = get_env_name("cache_dir");
let previous = env::var_os(&key);
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = env::temp_dir().join(format!("coyote-sandbox-vault-tests-{unique}"));
fs::create_dir_all(&path).unwrap();
unsafe {
env::set_var(&key, &path);
}
Self {
key,
previous,
path,
}
}
}
impl Drop for TestCacheDirGuard {
fn drop(&mut self) {
unsafe {
match &self.previous {
Some(v) => env::set_var(&self.key, v),
None => env::remove_var(&self.key),
}
}
let _ = fs::remove_dir_all(&self.path);
}
}
#[test] #[test]
fn returns_none_for_local() { fn returns_none_for_local() {
@@ -718,7 +664,6 @@ mod tests {
#[test] #[test]
#[serial] #[serial]
fn returns_some_for_aws() { fn returns_some_for_aws() {
let _guard = TestCacheDirGuard::new();
let p = SupportedProvider::AwsSecretsManager { let p = SupportedProvider::AwsSecretsManager {
provider_def: AwsSecretsManagerProvider { provider_def: AwsSecretsManagerProvider {
aws_profile: None, aws_profile: None,
@@ -735,7 +680,6 @@ mod tests {
#[test] #[test]
#[serial] #[serial]
fn returns_some_for_gcp() { fn returns_some_for_gcp() {
let _guard = TestCacheDirGuard::new();
let p = SupportedProvider::GcpSecretManager { let p = SupportedProvider::GcpSecretManager {
provider_def: GcpSecretManagerProvider { provider_def: GcpSecretManagerProvider {
gcp_project_id: None, gcp_project_id: None,
@@ -751,7 +695,6 @@ mod tests {
#[test] #[test]
#[serial] #[serial]
fn returns_some_for_one_password() { fn returns_some_for_one_password() {
let _guard = TestCacheDirGuard::new();
let p = SupportedProvider::OnePassword { let p = SupportedProvider::OnePassword {
provider_def: OnePasswordProvider { provider_def: OnePasswordProvider {
vault: None, vault: None,
@@ -768,7 +711,6 @@ mod tests {
#[test] #[test]
#[serial] #[serial]
fn returns_some_for_azure() { fn returns_some_for_azure() {
let _guard = TestCacheDirGuard::new();
let p = SupportedProvider::AzureKeyVault { let p = SupportedProvider::AzureKeyVault {
provider_def: AzureKeyVaultProvider { vault_name: None }, provider_def: AzureKeyVaultProvider { vault_name: None },
}; };
@@ -782,7 +724,6 @@ mod tests {
#[test] #[test]
#[serial] #[serial]
fn returns_some_for_gopass() { fn returns_some_for_gopass() {
let _guard = TestCacheDirGuard::new();
let p = SupportedProvider::Gopass { let p = SupportedProvider::Gopass {
provider_def: GopassProvider { store: None }, provider_def: GopassProvider { store: None },
}; };