fix(TASK-046): make inspection flags look-don't-touch and pin the contracts

- Skip the builtins bootstrap, function-binary build, and tool-scope
  rebuild for inspection-only runs (--info/--list-*/--mcp-list/
  --list-secrets without --agent/--role): a pristine COYOTE_CONFIG_DIR
  now stays byte-empty (was 269 files written before dispatch)
- Tolerate a model-less default config on the inspection surface via
  AppConfig::from_config_lenient + Model::default() in bootstrap;
  strict 'No available model' behavior unchanged for the REPL path and
  all mutating mcp/vault flags
- --list-secrets degrades to an rc=0 informational message in sandbox
  mode (mutating vault flags still fail there), pinned by test
- Add the 041.7-4 regression test: a malformed COYOTE_<NAME>_GLOBAL_TOOLS
  env override logs the debug message and leaves global_tools untouched
- Adopt scripts/usage-probe-inspection.sh (10 flags: rc=0 + zero writes,
  output required only for --info) and add binary-level
  tests/inspection_flags.rs, incl. the agent+--info still-bootstraps pin
This commit is contained in:
Dark-Alex-17 committed 2026-09-16 15:07:21 -06:00
1 parent 6514588e05
commit 9da0b91673
9 files changed
+389 -31

No files matched your search

+27
View File
@@ -1475,6 +1475,33 @@ global_hooks:
}
}
#[test]
#[serial_test::serial]
fn load_envs_ignores_malformed_global_tools_and_logs_debug() {
crate::testing::install_log_collector();
let name = "gt-malformed-envtest";
let yaml = format!("name: {name}\ninstructions: hi\nglobal_tools:\n - keep_tool.sh\n");
let mut config: AgentConfig = serde_yaml::from_str(&yaml).unwrap();
let env_name = normalize_env_name(&format!("{name}_global_tools"));
let _guard = crate::testing::EnvVarGuard::set(&env_name, "{not-json");
config.load_envs(&AppConfig::default());
assert_eq!(
config.global_tools,
vec!["keep_tool.sh"],
"a malformed override must leave the existing value untouched"
);
let debugs = crate::testing::debug_snapshot();
assert!(
debugs.iter().any(|message| {
message.contains("Ignoring malformed global_tools env override")
&& message.contains(name)
}),
"expected a debug log for the malformed override: {debugs:?}"
);
}
#[test]
fn from_graph_carries_hooks_and_global_hooks() {
let yaml = "\
+31 -1
View File
@@ -210,6 +210,18 @@ impl Default for AppConfig {
impl AppConfig {
pub fn from_config(config: super::Config) -> Result<Self> {
Self::from_config_impl(config, true)
}
/// Like [`Self::from_config`], but tolerates a config with no resolvable
/// model: `model_id` stays empty instead of failing with "No available
/// model". For inspection readouts (--info, --list-*, --mcp-list,
/// --list-secrets), which render model fields as absent/default.
pub fn from_config_lenient(config: super::Config) -> Result<Self> {
Self::from_config_impl(config, false)
}
fn from_config_impl(config: super::Config, require_model: bool) -> Result<Self> {
let mut app_config = Self {
model_id: config.model_id,
temperature: config.temperature,
@@ -306,7 +318,13 @@ impl AppConfig {
}
app_config.setup_document_loaders();
app_config.setup_user_agent();
app_config.resolve_model()?;
if require_model {
app_config.resolve_model()?;
} else {
// Model-less is fine here: model_id stays empty and inspection
// callers substitute Model::default().
let _ = app_config.resolve_model();
}
app_config.validate_reasoning_effort()?;
Ok(app_config)
}
@@ -1269,6 +1287,18 @@ mod tests {
assert!(result.is_err());
}
#[test]
fn from_config_errors_when_no_model_available() {
let result = AppConfig::from_config(Config::default());
assert!(result.is_err());
}
#[test]
fn from_config_lenient_tolerates_missing_model() {
let app = AppConfig::from_config_lenient(Config::default()).unwrap();
assert!(app.model_id.is_empty());
}
#[test]
fn resolve_model_keeps_explicit_model_id() {
let mut app = AppConfig {
+8 -1
View File
@@ -43,6 +43,7 @@ impl AppState {
log_path: Option<PathBuf>,
start_mcp_servers: bool,
lenient_vault: bool,
inspection: bool,
abort_signal: AbortSignal,
) -> Result<Self> {
let vault = if lenient_vault {
@@ -74,7 +75,13 @@ impl AppState {
}
}
let mut functions = Functions::init(config.visible_tools.as_deref())?;
// Inspection-only runs are look-don't-touch readouts: skip the
// function-binary prune/build, which writes into the config dir.
let mut functions = if inspection {
Functions::default()
} else {
Functions::init(config.visible_tools.as_deref())?
};
if !mcp_registry.is_empty() && config.mcp_server_support {
functions.append_mcp_meta_functions(mcp_registry.server_features());
}
+7 -1
View File
@@ -417,7 +417,13 @@ impl RequestContext {
working_mode: WorkingMode,
info_flag: bool,
) -> Result<Self> {
let model = Model::retrieve_model(&app.config, &app.config.model_id, ModelType::Chat)?;
// A lenient AppConfig (inspection-only run, no resolvable model)
// leaves model_id empty; render those readouts with a default model.
let model = if info_flag && app.config.model_id.is_empty() {
Model::default()
} else {
Model::retrieve_model(&app.config, &app.config.model_id, ModelType::Chat)?
};
let mut functions = app.functions.clone();
if working_mode.is_repl() {