Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/apps/desktop/src/api/remote_workspace_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] =
"get_global_config_status",
RemoteWorkspacePolicy::LegacyUnaudited,
),
(
"get_global_skill_settings",
RemoteWorkspacePolicy::WorkspaceAgnostic,
),
(
"get_health_status",
RemoteWorkspacePolicy::WorkspaceAgnostic,
Expand Down Expand Up @@ -1527,6 +1531,10 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] =
"set_external_subagent_activation_command",
RemoteWorkspacePolicy::RemoteUnsupported,
),
(
"set_global_skill_disabled",
RemoteWorkspacePolicy::WorkspaceAgnostic,
),
("set_macos_edit_menu_mode", RemoteWorkspacePolicy::LocalOnly),
(
"set_main_window_transient_geometry",
Expand Down
66 changes: 62 additions & 4 deletions src/apps/desktop/src/api/skill_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ use tokio::time::{timeout, Duration};

use crate::api::app_state::AppState;
use bitfun_core::agentic::tools::implementations::skills::mode_overrides::{
clear_user_mode_skill_overrides, load_project_mode_skills_document_local,
project_mode_skills_path_for_remote, save_project_mode_skills_document_local,
set_disabled_mode_skills_in_document, set_mode_skill_disabled_in_document,
set_user_mode_skill_state,
clear_user_mode_skill_overrides, load_globally_disabled_user_skills,
load_project_mode_skills_document_local, project_mode_skills_path_for_remote,
save_project_mode_skills_document_local, set_disabled_mode_skills_in_document,
set_global_user_skill_disabled, set_mode_skill_disabled_in_document, set_user_mode_skill_state,
};
use bitfun_core::agentic::tools::implementations::skills::{
resolver::resolve_skill_default_enabled_for_mode, ModeSkillInfo, SkillData, SkillInfo,
Expand Down Expand Up @@ -123,6 +123,19 @@ pub struct ResetModeSkillSelectionRequest {
pub workspace_path: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetGlobalSkillDisabledRequest {
pub skill_key: String,
pub disabled: bool,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GlobalSkillSettingsResponse {
pub globally_disabled_user_skill_keys: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillMarketItem {
Expand Down Expand Up @@ -497,6 +510,51 @@ pub async fn get_skill_configs(
.map_err(|e| format!("Failed to serialize skill configs: {}", e))
}

#[tauri::command]
pub async fn get_global_skill_settings() -> Result<GlobalSkillSettingsResponse, String> {
let globally_disabled_user_skill_keys = load_globally_disabled_user_skills()
.await
.map_err(|error| format!("Failed to load global Skill settings: {}", error))?;
Ok(GlobalSkillSettingsResponse {
globally_disabled_user_skill_keys,
})
}

#[tauri::command]
pub async fn set_global_skill_disabled(
request: SetGlobalSkillDisabledRequest,
) -> Result<GlobalSkillSettingsResponse, String> {
let skill_key = request.skill_key.trim();
if !skill_key.starts_with("user::") {
return Err("Global Skill availability only applies to user-level Skills".to_string());
}

let known_skill = SkillRegistry::global()
.get_all_skills()
.await
.into_iter()
.any(|skill| skill.key == skill_key && skill.level == SkillLocation::User);
if !known_skill {
return Err(format!("User-level Skill '{}' was not found", skill_key));
}

let globally_disabled_user_skill_keys =
set_global_user_skill_disabled(skill_key, request.disabled)
.await
.map_err(|error| format!("Failed to update global Skill settings: {}", error))?;
if let Err(error) = bitfun_core::service::config::reload_global_config().await {
log::warn!(
"Failed to reload global configuration after Skill availability update: skill_key={}, error={}",
skill_key,
error
);
}

Ok(GlobalSkillSettingsResponse {
globally_disabled_user_skill_keys,
})
}

#[tauri::command]
pub async fn get_mode_skill_configs(
state: State<'_, AppState>,
Expand Down
2 changes: 2 additions & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1301,10 +1301,12 @@ pub async fn run() {
list_agent_tool_names,
update_subagent_config,
get_skill_configs,
get_global_skill_settings,
get_mode_skill_configs,
list_skill_market,
search_skill_market,
download_skill_market,
set_global_skill_disabled,
set_mode_skill_disabled,
replace_mode_skill_selection,
reset_mode_skill_selection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ use crate::service::config::agent_profile_project_store::{
};
use crate::service::config::global::GlobalConfigManager;
use crate::service::config::mode_config_canonicalizer::persist_agent_profile_from_value;
use crate::service::config::types::AgentProfileConfig;
use crate::service::config::types::{AgentProfileConfig, SkillSettingsConfig};
use crate::util::errors::{BitFunError, BitFunResult};
use bitfun_agent_runtime::skills::normalize_user_mode_skill_overrides;
pub use bitfun_agent_runtime::skills::UserModeSkillOverrides;
use bitfun_agent_runtime::skills::{normalize_skill_keys, normalize_user_mode_skill_overrides};
use serde_json::json;
use std::collections::HashMap;
use std::path::Path;
Expand Down Expand Up @@ -89,6 +89,48 @@ pub async fn clear_user_mode_skill_overrides(
load_user_mode_skill_overrides(mode_id).await
}

pub async fn load_globally_disabled_user_skills() -> BitFunResult<Vec<String>> {
let config_service = GlobalConfigManager::get_service().await?;
let settings: SkillSettingsConfig = config_service
.get_config(Some("ai.skill_settings"))
.await
.unwrap_or_default();
Ok(normalize_skill_keys(settings.globally_disabled_user_skills))
}

pub async fn set_global_user_skill_disabled(
skill_key: &str,
disabled: bool,
) -> BitFunResult<Vec<String>> {
let skill_key = skill_key.trim();
if skill_key.is_empty() {
return Ok(Vec::new());
}

let config_service = GlobalConfigManager::get_service().await?;
let mut settings: SkillSettingsConfig = config_service
.get_config(Some("ai.skill_settings"))
.await
.unwrap_or_default();

if disabled {
settings
.globally_disabled_user_skills
.push(skill_key.to_string());
} else {
settings
.globally_disabled_user_skills
.retain(|key| key != skill_key);
}
settings.globally_disabled_user_skills =
normalize_skill_keys(settings.globally_disabled_user_skills);

config_service
.set_config("ai.skill_settings", &settings)
.await?;
Ok(settings.globally_disabled_user_skills)
}

pub fn project_mode_skills_path_for_remote(remote_root: &str) -> String {
project_agent_profiles_path_for_remote(remote_root)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
use super::builtin::ensure_builtin_skills_installed;
use super::mode_overrides::{
load_disabled_mode_skills_local, load_disabled_mode_skills_remote,
load_user_mode_skill_overrides, UserModeSkillOverrides,
load_globally_disabled_user_skills, load_user_mode_skill_overrides, UserModeSkillOverrides,
};
use super::types::{ModeSkillInfo, SkillData, SkillInfo, SkillLocation};
use crate::agentic::workspace::WorkspaceFileSystem;
use crate::infrastructure::get_path_manager_arc;
use crate::util::errors::{BitFunError, BitFunResult};
use bitfun_agent_runtime::skills::{
annotate_shadowed_skills, build_mode_skill_infos, filter_candidates_for_mode,
filter_implicitly_invocable_skills, normalize_local_skill_dir_name,
filter_implicitly_invocable_skills, is_skill_globally_enabled, normalize_local_skill_dir_name,
normalize_remote_skill_dir_name, normalize_skill_keys,
resolve_default_hidden_builtin_for_explicit_invocation, resolve_user_config_skill_root,
resolve_visible_skills, sort_skill_candidates_by_dir, sort_skills,
Expand Down Expand Up @@ -79,6 +79,26 @@ impl SkillRegistry {
SKILL_REGISTRY.get_or_init(Self::new)
}

async fn globally_disabled_user_skill_keys() -> HashSet<String> {
load_globally_disabled_user_skills()
.await
.unwrap_or_default()
.into_iter()
.collect()
}

fn filter_globally_disabled_candidates(
candidates: Vec<SkillCandidate>,
globally_disabled_user_skills: &HashSet<String>,
) -> Vec<SkillCandidate> {
candidates
.into_iter()
.filter(|candidate| {
is_skill_globally_enabled(&candidate.info, globally_disabled_user_skills)
})
.collect()
}

async fn apply_local_openai_policy(skill_data: &mut SkillData, skill_dir: &Path) {
let policy_path = skill_dir.join("agents").join("openai.yaml");
let content = match fs::read_to_string(&policy_path).await {
Expand Down Expand Up @@ -431,6 +451,9 @@ impl SkillRegistry {
workspace_root: Option<&Path>,
agent_type: Option<&str>,
) -> Vec<SkillCandidate> {
let globally_disabled_user_skills = Self::globally_disabled_user_skill_keys().await;
let candidates =
Self::filter_globally_disabled_candidates(candidates, &globally_disabled_user_skills);
let Some(mode_id) = agent_type.map(str::trim).filter(|value| !value.is_empty()) else {
return candidates;
};
Expand Down Expand Up @@ -458,6 +481,9 @@ impl SkillRegistry {
remote_root: &str,
agent_type: Option<&str>,
) -> Vec<SkillCandidate> {
let globally_disabled_user_skills = Self::globally_disabled_user_skill_keys().await;
let candidates =
Self::filter_globally_disabled_candidates(candidates, &globally_disabled_user_skills);
let Some(mode_id) = agent_type.map(str::trim).filter(|value| !value.is_empty()) else {
return candidates;
};
Expand Down Expand Up @@ -506,6 +532,9 @@ impl SkillRegistry {
let candidates = self
.scan_skill_candidates_for_workspace(workspace_root)
.await;
let globally_disabled_user_skills = Self::globally_disabled_user_skill_keys().await;
let candidates =
Self::filter_globally_disabled_candidates(candidates, &globally_disabled_user_skills);
let filtered = self
.apply_mode_filters_for_workspace(candidates.clone(), workspace_root, agent_type)
.await;
Expand All @@ -531,6 +560,9 @@ impl SkillRegistry {
let candidates = self
.scan_skill_candidates_for_remote_workspace(fs, remote_root)
.await;
let globally_disabled_user_skills = Self::globally_disabled_user_skill_keys().await;
let candidates =
Self::filter_globally_disabled_candidates(candidates, &globally_disabled_user_skills);
let filtered = self
.apply_mode_filters_for_remote_workspace(
candidates.clone(),
Expand Down Expand Up @@ -670,8 +702,11 @@ impl SkillRegistry {
};
let disabled_project: HashSet<String> =
normalize_skill_keys(disabled_project).into_iter().collect();
let filtered =
filter_candidates_for_mode(candidates, mode_id, &user_overrides, &disabled_project);
let globally_disabled_user_skills = Self::globally_disabled_user_skill_keys().await;
let filtered = Self::filter_globally_disabled_candidates(
filter_candidates_for_mode(candidates, mode_id, &user_overrides, &disabled_project),
&globally_disabled_user_skills,
);
let resolved = resolve_visible_skills(filtered);

build_mode_skill_infos(
Expand All @@ -680,6 +715,7 @@ impl SkillRegistry {
mode_id,
&user_overrides,
&disabled_project,
&globally_disabled_user_skills,
)
}

Expand All @@ -701,8 +737,11 @@ impl SkillRegistry {
.unwrap_or_default();
let disabled_project: HashSet<String> =
normalize_skill_keys(disabled_project).into_iter().collect();
let filtered =
filter_candidates_for_mode(candidates, mode_id, &user_overrides, &disabled_project);
let globally_disabled_user_skills = Self::globally_disabled_user_skill_keys().await;
let filtered = Self::filter_globally_disabled_candidates(
filter_candidates_for_mode(candidates, mode_id, &user_overrides, &disabled_project),
&globally_disabled_user_skills,
);
let resolved = resolve_visible_skills(filtered);

build_mode_skill_infos(
Expand All @@ -711,6 +750,7 @@ impl SkillRegistry {
mode_id,
&user_overrides,
&disabled_project,
&globally_disabled_user_skills,
)
}

Expand Down
14 changes: 14 additions & 0 deletions src/crates/assembly/core/src/service/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,10 @@ pub struct AIConfig {
#[serde(default, deserialize_with = "deserialize_agent_profiles")]
pub agent_profiles: HashMap<String, AgentProfileConfig>,

/// User-level Skill availability shared by every agent profile.
#[serde(default)]
pub skill_settings: SkillSettingsConfig,

/// Review team configuration.
/// team_id -> ReviewTeamConfig
#[serde(default = "default_review_team_configs")]
Expand Down Expand Up @@ -978,6 +982,15 @@ pub struct AgentProfileConfig {
pub tool_permission_rules: Vec<PermissionRule>,
}

/// User-level Skill configuration shared by every agent profile.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct SkillSettingsConfig {
/// User-level Skill keys disabled for every agent profile.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub globally_disabled_user_skills: Vec<String>,
}

/// API view of a mode configuration.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
Expand Down Expand Up @@ -1839,6 +1852,7 @@ impl Default for AIConfig {
default_models: DefaultModelsConfig::default(),
agent_model_defaults: AgentModelDefaultsConfig::default(),
agent_profiles: std::collections::HashMap::new(),
skill_settings: SkillSettingsConfig::default(),
review_teams: default_review_team_configs(),
review_team_rate_limit_status: default_review_team_rate_limit_status(),
subagent_max_concurrency: default_subagent_max_concurrency(),
Expand Down
2 changes: 1 addition & 1 deletion src/crates/execution/agent-runtime/src/skills/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub use roots::{
};
pub use selection::{
annotate_shadowed_skills, build_mode_skill_infos, filter_candidates_for_mode,
filter_implicitly_invocable_skills, normalize_skill_keys,
filter_implicitly_invocable_skills, is_skill_globally_enabled, normalize_skill_keys,
resolve_default_hidden_builtin_for_explicit_invocation, resolve_visible_skills,
sort_skill_candidates_by_dir, sort_skills, ExplicitSkillInvocationResolution, SkillCandidate,
};
Expand Down
12 changes: 11 additions & 1 deletion src/crates/execution/agent-runtime/src/skills/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ pub fn filter_implicitly_invocable_skills(skills: Vec<SkillInfo>) -> Vec<SkillIn
.collect()
}

pub fn is_skill_globally_enabled(
skill: &SkillInfo,
globally_disabled_user_skills: &HashSet<String>,
) -> bool {
skill.level != SkillLocation::User || !globally_disabled_user_skills.contains(&skill.key)
}

pub fn filter_candidates_for_mode(
candidates: Vec<SkillCandidate>,
mode_id: &str,
Expand Down Expand Up @@ -206,6 +213,7 @@ pub fn build_mode_skill_infos(
mode_id: &str,
user_overrides: &UserModeSkillOverrides,
disabled_project_skills: &HashSet<String>,
globally_disabled_user_skills: &HashSet<String>,
) -> Vec<ModeSkillInfo> {
let resolved_by_name: HashMap<String, String> = resolved_skills
.iter()
Expand All @@ -223,7 +231,8 @@ pub fn build_mode_skill_infos(
user_overrides,
disabled_project_skills,
);
let selected_for_runtime = resolved_keys.contains(&skill.key);
let globally_enabled = is_skill_globally_enabled(&skill, globally_disabled_user_skills);
let selected_for_runtime = globally_enabled && resolved_keys.contains(&skill.key);
let mode_winner_key = state
.effective_enabled
.then(|| resolved_by_name.get(&skill.name))
Expand All @@ -237,6 +246,7 @@ pub fn build_mode_skill_infos(
ModeSkillInfo {
skill,
default_enabled: state.default_enabled,
globally_enabled,
effective_enabled: state.effective_enabled,
disabled_by_mode: !state.effective_enabled,
selected_for_runtime,
Expand Down
Loading