feat: add --no-workspace-macros opt-out for workspace macro loading
Mirrors --no-workspace-mcp exactly: a CLI-only flag backed by an AppConfig field (default false) that disables .coyote/macros in both the resolved macro policy and Macro::load's workspace-then-global preference, so the two always agree (custom-commands design §5).
This commit is contained in:
@@ -96,6 +96,9 @@ pub struct Cli {
|
|||||||
/// Disable loading workspace MCP servers from .coyote/mcp.json, .coyote/.mcp.json, or .mcp.json
|
/// Disable loading workspace MCP servers from .coyote/mcp.json, .coyote/.mcp.json, or .mcp.json
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_workspace_mcp: bool,
|
pub no_workspace_mcp: bool,
|
||||||
|
/// Disable loading workspace macros from .coyote/macros
|
||||||
|
#[arg(long)]
|
||||||
|
pub no_workspace_macros: bool,
|
||||||
/// Disable memory for this invocation
|
/// Disable memory for this invocation
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_memory: bool,
|
pub no_memory: bool,
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ pub struct AppConfig {
|
|||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<String>,
|
||||||
pub save_shell_history: bool,
|
pub save_shell_history: bool,
|
||||||
pub no_workspace_mcp: bool,
|
pub no_workspace_mcp: bool,
|
||||||
|
pub no_workspace_macros: bool,
|
||||||
pub sync_models_url: Option<String>,
|
pub sync_models_url: Option<String>,
|
||||||
|
|
||||||
pub clients: Vec<ClientConfig>,
|
pub clients: Vec<ClientConfig>,
|
||||||
@@ -181,6 +182,7 @@ impl Default for AppConfig {
|
|||||||
user_agent: None,
|
user_agent: None,
|
||||||
save_shell_history: true,
|
save_shell_history: true,
|
||||||
no_workspace_mcp: false,
|
no_workspace_mcp: false,
|
||||||
|
no_workspace_macros: false,
|
||||||
sync_models_url: None,
|
sync_models_url: None,
|
||||||
|
|
||||||
clients: vec![],
|
clients: vec![],
|
||||||
@@ -266,6 +268,7 @@ impl AppConfig {
|
|||||||
user_agent: config.user_agent,
|
user_agent: config.user_agent,
|
||||||
save_shell_history: config.save_shell_history,
|
save_shell_history: config.save_shell_history,
|
||||||
no_workspace_mcp: false,
|
no_workspace_mcp: false,
|
||||||
|
no_workspace_macros: false,
|
||||||
sync_models_url: config.sync_models_url,
|
sync_models_url: config.sync_models_url,
|
||||||
|
|
||||||
clients: config.clients,
|
clients: config.clients,
|
||||||
|
|||||||
@@ -855,6 +855,39 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn effective_honors_no_workspace_macros() {
|
||||||
|
with_macro_dirs(|workspace_root, global| {
|
||||||
|
let macros_subdir = workspace_root.join("macros");
|
||||||
|
fs::create_dir_all(¯os_subdir).unwrap();
|
||||||
|
write_macro(¯os_subdir, "shared", VALID_YAML);
|
||||||
|
write_macro(¯os_subdir, "ws-only", VALID_YAML);
|
||||||
|
write_macro(global, "shared", VALID_YAML);
|
||||||
|
write_macro(global, "global-only", VALID_YAML);
|
||||||
|
|
||||||
|
with_macro_dir_envs(workspace_root, global, || {
|
||||||
|
let config = AppConfig::default();
|
||||||
|
|
||||||
|
let policy = MacroPolicy::effective(&config, None, None, None, &[], false);
|
||||||
|
assert_eq!(
|
||||||
|
policy.find("shared").unwrap().source,
|
||||||
|
Some(MacroSource::Workspace)
|
||||||
|
);
|
||||||
|
assert!(policy.find("ws-only").is_some());
|
||||||
|
assert!(policy.find("global-only").is_some());
|
||||||
|
|
||||||
|
let policy = MacroPolicy::effective(&config, None, None, None, &[], true);
|
||||||
|
assert_eq!(
|
||||||
|
policy.find("shared").unwrap().source,
|
||||||
|
Some(MacroSource::Global)
|
||||||
|
);
|
||||||
|
assert!(policy.find("ws-only").is_none());
|
||||||
|
assert!(policy.find("global-only").is_some());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn effective_resolves_role_session_and_global_levels() {
|
fn effective_resolves_role_session_and_global_levels() {
|
||||||
|
|||||||
+68
-3
@@ -25,7 +25,7 @@ pub async fn macro_execute(
|
|||||||
if ctx.in_non_isolated_macro() {
|
if ctx.in_non_isolated_macro() {
|
||||||
bail!("nested macros not allowed in non-isolated mode");
|
bail!("nested macros not allowed in non-isolated mode");
|
||||||
}
|
}
|
||||||
let macro_value = Macro::load(name)?;
|
let macro_value = Macro::load(name, ctx.app.config.no_workspace_macros)?;
|
||||||
let (mut new_args, text) = split_args_text(args.unwrap_or_default(), cfg!(windows));
|
let (mut new_args, text) = split_args_text(args.unwrap_or_default(), cfg!(windows));
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
new_args.push(text.to_string());
|
new_args.push(text.to_string());
|
||||||
@@ -140,9 +140,9 @@ pub struct Macro {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Macro {
|
impl Macro {
|
||||||
pub fn load(name: &str) -> Result<Macro> {
|
pub fn load(name: &str, no_workspace_macros: bool) -> Result<Macro> {
|
||||||
let workspace_path = paths::workspace_macros_dir().join(format!("{name}.yaml"));
|
let workspace_path = paths::workspace_macros_dir().join(format!("{name}.yaml"));
|
||||||
let path = if workspace_path.exists() {
|
let path = if !no_workspace_macros && workspace_path.exists() {
|
||||||
workspace_path
|
workspace_path
|
||||||
} else {
|
} else {
|
||||||
paths::macro_file(name)
|
paths::macro_file(name)
|
||||||
@@ -307,6 +307,71 @@ mod tests {
|
|||||||
write(&path, content).unwrap();
|
write(&path, content).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets up a temp workspace macros dir and a temp global macros dir, each
|
||||||
|
/// containing a `shared` macro whose `description` names its source, and
|
||||||
|
/// points the workspace/global dir env overrides at them for `f`.
|
||||||
|
fn with_macro_load_envs<F: FnOnce()>(f: F) {
|
||||||
|
let unique = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let root = env::temp_dir().join(format!("coyote-macro-load-tests-{unique}"));
|
||||||
|
let workspace_root = root.join("workspace");
|
||||||
|
let workspace_macros = workspace_root.join("macros");
|
||||||
|
let global = root.join("global");
|
||||||
|
create_dir_all(&workspace_macros).unwrap();
|
||||||
|
create_dir_all(&global).unwrap();
|
||||||
|
write(
|
||||||
|
workspace_macros.join("shared.yaml"),
|
||||||
|
"description: workspace\nsteps:\n - \".help\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
write(
|
||||||
|
global.join("shared.yaml"),
|
||||||
|
"description: global\nsteps:\n - \".help\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ws_env = get_env_name("workspace_config_dir");
|
||||||
|
let global_env = get_env_name("macros_dir");
|
||||||
|
let prev_ws = env::var_os(&ws_env);
|
||||||
|
let prev_global = env::var_os(&global_env);
|
||||||
|
unsafe {
|
||||||
|
env::set_var(&ws_env, &workspace_root);
|
||||||
|
env::set_var(&global_env, &global);
|
||||||
|
}
|
||||||
|
f();
|
||||||
|
unsafe {
|
||||||
|
match prev_ws {
|
||||||
|
Some(v) => env::set_var(&ws_env, v),
|
||||||
|
None => env::remove_var(&ws_env),
|
||||||
|
}
|
||||||
|
match prev_global {
|
||||||
|
Some(v) => env::set_var(&global_env, v),
|
||||||
|
None => env::remove_var(&global_env),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn load_prefers_workspace_over_global_by_default() {
|
||||||
|
with_macro_load_envs(|| {
|
||||||
|
let loaded = Macro::load("shared", false).unwrap();
|
||||||
|
assert_eq!(loaded.description.as_deref(), Some("workspace"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn load_skips_workspace_when_no_workspace_macros() {
|
||||||
|
with_macro_load_envs(|| {
|
||||||
|
let loaded = Macro::load("shared", true).unwrap();
|
||||||
|
assert_eq!(loaded.description.as_deref(), Some("global"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Drives a macro-execution future to completion on a thread with extra
|
/// Drives a macro-execution future to completion on a thread with extra
|
||||||
/// stack headroom: nested `run_repl_command` poll frames are deep in
|
/// stack headroom: nested `run_repl_command` poll frames are deep in
|
||||||
/// debug builds and overflow the 2 MiB default test-thread stack.
|
/// debug builds and overflow the 2 MiB default test-thread stack.
|
||||||
|
|||||||
@@ -2505,7 +2505,7 @@ impl RequestContext {
|
|||||||
self.agent.as_ref(),
|
self.agent.as_ref(),
|
||||||
self.session.as_ref(),
|
self.session.as_ref(),
|
||||||
&crate::repl::builtin_command_names(),
|
&crate::repl::builtin_command_names(),
|
||||||
false,
|
self.app.config.no_workspace_macros,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -222,6 +222,9 @@ async fn main() -> Result<()> {
|
|||||||
if cli.no_workspace_mcp {
|
if cli.no_workspace_mcp {
|
||||||
app_config.no_workspace_mcp = true;
|
app_config.no_workspace_mcp = true;
|
||||||
}
|
}
|
||||||
|
if cli.no_workspace_macros {
|
||||||
|
app_config.no_workspace_macros = true;
|
||||||
|
}
|
||||||
let app_config: Arc<AppConfig> = Arc::new(app_config);
|
let app_config: Arc<AppConfig> = Arc::new(app_config);
|
||||||
let app_state: Arc<AppState> = Arc::new(
|
let app_state: Arc<AppState> = Arc::new(
|
||||||
AppState::init(
|
AppState::init(
|
||||||
|
|||||||
Reference in New Issue
Block a user