Skip to content

Commit a7b3801

Browse files
authored
Merge pull request #663 from moltis-org/precious-tibia
2 parents 5afb1bc + 69a6141 commit a7b3801

1 file changed

Lines changed: 116 additions & 45 deletions

File tree

crates/chat/src/lib.rs

Lines changed: 116 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,28 @@ fn prompt_build_limits_from_config(config: &moltis_config::MoltisConfig) -> Prom
11511151
}
11521152
}
11531153

1154+
/// Discover skills from the default filesystem paths, honoring `[skills] enabled`.
1155+
///
1156+
/// Returns an empty list when `config.skills.enabled` is `false`, so callers can
1157+
/// unconditionally feed the result into prompt building / tool filtering without
1158+
/// injecting skills into the LLM context when the operator has disabled them.
1159+
async fn discover_skills_if_enabled(
1160+
config: &moltis_config::MoltisConfig,
1161+
) -> Vec<moltis_skills::types::SkillMetadata> {
1162+
if !config.skills.enabled {
1163+
return Vec::new();
1164+
}
1165+
let search_paths = moltis_skills::discover::FsSkillDiscoverer::default_paths();
1166+
let discoverer = moltis_skills::discover::FsSkillDiscoverer::new(search_paths);
1167+
match discoverer.discover().await {
1168+
Ok(skills) => skills,
1169+
Err(e) => {
1170+
warn!("failed to discover skills: {e}");
1171+
Vec::new()
1172+
},
1173+
}
1174+
}
1175+
11541176
fn resolve_channel_runtime_context(
11551177
session_key: &str,
11561178
session_entry: Option<&SessionEntry>,
@@ -3707,16 +3729,10 @@ impl ChatService for LiveChatService {
37073729
run_id: Some(run_id.clone()),
37083730
};
37093731

3710-
// Discover enabled skills/plugins for prompt injection.
3711-
let search_paths = moltis_skills::discover::FsSkillDiscoverer::default_paths();
3712-
let discoverer = moltis_skills::discover::FsSkillDiscoverer::new(search_paths);
3713-
let discovered_skills = match discoverer.discover().await {
3714-
Ok(s) => s,
3715-
Err(e) => {
3716-
warn!("failed to discover skills: {e}");
3717-
Vec::new()
3718-
},
3719-
};
3732+
// Discover enabled skills/plugins for prompt injection (gated on
3733+
// `[skills] enabled` — see #655).
3734+
let discovered_skills =
3735+
discover_skills_if_enabled(&moltis_config::discover_and_load()).await;
37203736

37213737
// Check if MCP tools are disabled for this session and capture
37223738
// per-session sandbox override details for prompt runtime context.
@@ -5014,23 +5030,20 @@ impl ChatService for LiveChatService {
50145030
"promptSymbol": exec_prompt_symbol,
50155031
});
50165032

5017-
// Discover enabled skills/plugins (only if provider supports tools)
5033+
// Discover enabled skills/plugins (only if provider supports tools and
5034+
// `[skills] enabled` is true — see #655).
50185035
let skills_list: Vec<Value> = if supports_tools {
5019-
let search_paths = moltis_skills::discover::FsSkillDiscoverer::default_paths();
5020-
let discoverer = moltis_skills::discover::FsSkillDiscoverer::new(search_paths);
5021-
match discoverer.discover().await {
5022-
Ok(s) => s
5023-
.iter()
5024-
.map(|s| {
5025-
serde_json::json!({
5026-
"name": s.name,
5027-
"description": s.description,
5028-
"source": s.source,
5029-
})
5036+
discover_skills_if_enabled(&config)
5037+
.await
5038+
.iter()
5039+
.map(|s| {
5040+
serde_json::json!({
5041+
"name": s.name,
5042+
"description": s.description,
5043+
"source": s.source,
50305044
})
5031-
.collect(),
5032-
Err(_) => vec![],
5033-
}
5045+
})
5046+
.collect()
50345047
} else {
50355048
vec![]
50365049
};
@@ -5108,16 +5121,8 @@ impl ChatService for LiveChatService {
51085121
.resolve_project_context(&session_key, conn_id.as_deref())
51095122
.await;
51105123

5111-
// Discover skills.
5112-
let search_paths = moltis_skills::discover::FsSkillDiscoverer::default_paths();
5113-
let discoverer = moltis_skills::discover::FsSkillDiscoverer::new(search_paths);
5114-
let discovered_skills = match discoverer.discover().await {
5115-
Ok(s) => s,
5116-
Err(e) => {
5117-
warn!("failed to discover skills: {e}");
5118-
Vec::new()
5119-
},
5120-
};
5124+
// Discover skills (gated on `[skills] enabled` — see #655).
5125+
let discovered_skills = discover_skills_if_enabled(&persona.config).await;
51215126

51225127
// Check MCP disabled.
51235128
let mcp_disabled = session_entry
@@ -5233,16 +5238,8 @@ impl ChatService for LiveChatService {
52335238
.resolve_project_context(&session_key, conn_id.as_deref())
52345239
.await;
52355240

5236-
// Discover skills.
5237-
let search_paths = moltis_skills::discover::FsSkillDiscoverer::default_paths();
5238-
let discoverer = moltis_skills::discover::FsSkillDiscoverer::new(search_paths);
5239-
let discovered_skills = match discoverer.discover().await {
5240-
Ok(s) => s,
5241-
Err(e) => {
5242-
warn!("failed to discover skills: {e}");
5243-
Vec::new()
5244-
},
5245-
};
5241+
// Discover skills (gated on `[skills] enabled` — see #655).
5242+
let discovered_skills = discover_skills_if_enabled(&persona.config).await;
52465243

52475244
// Check MCP disabled.
52485245
let mcp_disabled = session_entry
@@ -14221,4 +14218,78 @@ mod tests {
1422114218
"error should mention timeout: {err}"
1422214219
);
1422314220
}
14221+
14222+
/// Serializes tests that mutate the global `moltis_config` data_dir
14223+
/// override so they don't race within the chat crate's test binary.
14224+
/// A `Semaphore` with a single permit is used here instead of
14225+
/// `Mutex<()>` because CLAUDE.md forbids bare `Mutex<()>` — a mutex must
14226+
/// guard real state. This semaphore is a pure serialization primitive
14227+
/// for a separately-owned global (`moltis_config`'s data_dir override).
14228+
static SKILLS_TEST_DATA_DIR_LOCK: Semaphore = Semaphore::const_new(1);
14229+
14230+
/// Regression test for #655: `[skills] enabled = false` must short-circuit
14231+
/// skill discovery so nothing from the filesystem ends up in the LLM prompt.
14232+
#[tokio::test]
14233+
async fn discover_skills_if_enabled_short_circuits_when_disabled() {
14234+
let _permit = SKILLS_TEST_DATA_DIR_LOCK
14235+
.acquire()
14236+
.await
14237+
.expect("semaphore closed");
14238+
14239+
// Point data_dir at a temp dir containing a real SKILL.md so that if
14240+
// the helper *did* fall through to the discoverer, it would return a
14241+
// non-empty list and fail this assertion.
14242+
let tmp = tempfile::tempdir().expect("tempdir");
14243+
let skills_dir = tmp.path().join("skills").join("planted-skill");
14244+
std::fs::create_dir_all(&skills_dir).expect("mkdir");
14245+
std::fs::write(
14246+
skills_dir.join("SKILL.md"),
14247+
"---\nname: planted-skill\ndescription: should not appear\n---\nbody",
14248+
)
14249+
.expect("write");
14250+
moltis_config::set_data_dir(tmp.path().to_path_buf());
14251+
14252+
let mut cfg = moltis_config::MoltisConfig::default();
14253+
cfg.skills.enabled = false;
14254+
14255+
let result = discover_skills_if_enabled(&cfg).await;
14256+
moltis_config::clear_data_dir();
14257+
14258+
assert!(
14259+
result.is_empty(),
14260+
"disabled skills must yield no discovered skills, got: {result:?}",
14261+
);
14262+
}
14263+
14264+
/// Complement to the short-circuit test: when enabled, the helper actually
14265+
/// invokes the filesystem discoverer. Validates the other arm of the
14266+
/// `enabled` branch so we don't accidentally hard-code `Vec::new()`.
14267+
#[tokio::test]
14268+
async fn discover_skills_if_enabled_runs_discoverer_when_enabled() {
14269+
let _permit = SKILLS_TEST_DATA_DIR_LOCK
14270+
.acquire()
14271+
.await
14272+
.expect("semaphore closed");
14273+
14274+
let tmp = tempfile::tempdir().expect("tempdir");
14275+
let skills_dir = tmp.path().join("skills").join("live-skill");
14276+
std::fs::create_dir_all(&skills_dir).expect("mkdir");
14277+
std::fs::write(
14278+
skills_dir.join("SKILL.md"),
14279+
"---\nname: live-skill\ndescription: visible to prompt\n---\nbody",
14280+
)
14281+
.expect("write");
14282+
moltis_config::set_data_dir(tmp.path().to_path_buf());
14283+
14284+
let mut cfg = moltis_config::MoltisConfig::default();
14285+
cfg.skills.enabled = true;
14286+
14287+
let result = discover_skills_if_enabled(&cfg).await;
14288+
moltis_config::clear_data_dir();
14289+
14290+
assert!(
14291+
result.iter().any(|s| s.name == "live-skill"),
14292+
"enabled skills must be discovered from data_dir, got: {result:?}",
14293+
);
14294+
}
1422414295
}

0 commit comments

Comments
 (0)