From 0e09711e56d89d1e1d410f53c3f492659e042c1f Mon Sep 17 00:00:00 2001 From: xintaofei Date: Fri, 24 Apr 2026 14:18:36 +0800 Subject: [PATCH] feat(codex-skills): surface built-in .system skills as read-only Scan ~/.codex/skills/.system so Codex CLI's bundled skills (imagegen, skill-creator, etc.) appear in the $ autocomplete and Skills settings. Mark them read_only on the API; the save/delete handlers refuse writes to that path, and the UI shows a system badge with a tooltip and disables edit/delete/save for those entries. --- src-tauri/src/acp/types.rs | 4 ++ src-tauri/src/commands/acp.rs | 42 ++++++++++++++++++++- src/components/settings/skills-settings.tsx | 39 +++++++++++++++++-- src/i18n/messages/ar.json | 2 + src/i18n/messages/de.json | 2 + src/i18n/messages/en.json | 2 + src/i18n/messages/es.json | 2 + src/i18n/messages/fr.json | 2 + src/i18n/messages/ja.json | 2 + src/i18n/messages/ko.json | 2 + src/i18n/messages/pt.json | 2 + src/i18n/messages/zh-CN.json | 2 + src/i18n/messages/zh-TW.json | 2 + src/lib/types.ts | 1 + 14 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/acp/types.rs b/src-tauri/src/acp/types.rs index faa145a..56748d1 100644 --- a/src-tauri/src/acp/types.rs +++ b/src-tauri/src/acp/types.rs @@ -302,6 +302,10 @@ pub struct AgentSkillItem { pub scope: AgentSkillScope, pub layout: AgentSkillLayout, pub path: String, + /// True for skills bundled by the agent CLI itself (e.g. Codex's + /// `~/.codex/skills/.system/*`). Surfaced so the UI can show them but + /// refuse to edit or delete; the backend also refuses such writes. + pub read_only: bool, } #[derive(Debug, Clone, Serialize)] diff --git a/src-tauri/src/commands/acp.rs b/src-tauri/src/commands/acp.rs index 549ab04..a2f64f9 100644 --- a/src-tauri/src/commands/acp.rs +++ b/src-tauri/src/commands/acp.rs @@ -1397,6 +1397,13 @@ pub(crate) fn skill_storage_spec(agent_type: AgentType) -> Option bool { + if agent_type != AgentType::Codex { + return false; + } + let ro_root = codex_home_dir().join("skills").join(".system"); + skill_path.starts_with(&ro_root) +} + fn skill_content_path(layout: AgentSkillLayout, skill_path: &Path) -> PathBuf { match layout { AgentSkillLayout::SkillDirectory => skill_path.join("SKILL.md"), @@ -2967,6 +2987,11 @@ pub async fn acp_list_agent_skills( } let mut skills = skills_by_key.into_values().collect::>(); + for skill in &mut skills { + if is_read_only_skill_path(agent_type, Path::new(&skill.path)) { + skill.read_only = true; + } + } skills.sort_by(|a, b| { scope_rank(a.scope) .cmp(&scope_rank(b.scope)) @@ -2996,8 +3021,11 @@ pub async fn acp_read_agent_skill( let id = validate_skill_id(&skill_id)?; let dirs = scoped_skill_dirs(agent_type, scope, workspace_path.as_deref())?; - let skill = locate_existing_skill_across_dirs(&dirs, spec.kind, &id, scope) + let mut skill = locate_existing_skill_across_dirs(&dirs, spec.kind, &id, scope) .ok_or_else(|| AcpError::protocol(format!("skill not found: {id}")))?; + if is_read_only_skill_path(agent_type, Path::new(&skill.path)) { + skill.read_only = true; + } let content_path = skill_content_path(skill.layout, Path::new(&skill.path)); let content = fs::read_to_string(&content_path) .map_err(|e| AcpError::protocol(format!("failed to read skill content: {e}")))?; @@ -3026,6 +3054,13 @@ pub async fn acp_save_agent_skill( .map_err(|e| AcpError::protocol(format!("failed to create skills directory: {e}")))?; let existing = locate_existing_skill_across_dirs(&dirs, spec.kind, &id, scope); + if let Some(ref item) = existing { + if is_read_only_skill_path(agent_type, Path::new(&item.path)) { + return Err(AcpError::protocol(format!( + "skill '{id}' is a built-in system skill and cannot be modified" + ))); + } + } let skill = if let Some(item) = existing { item } else { @@ -3081,6 +3116,11 @@ pub async fn acp_delete_agent_skill( let skill = locate_existing_skill_across_dirs(&dirs, spec.kind, &id, scope) .ok_or_else(|| AcpError::protocol(format!("skill not found: {id}")))?; + if is_read_only_skill_path(agent_type, Path::new(&skill.path)) { + return Err(AcpError::protocol(format!( + "skill '{id}' is a built-in system skill and cannot be deleted" + ))); + } let skill_path = PathBuf::from(&skill.path); remove_skill_entry(&skill_path) .map_err(|e| AcpError::protocol(format!("failed to delete skill entry: {e}")))?; diff --git a/src/components/settings/skills-settings.tsx b/src/components/settings/skills-settings.tsx index 75b6ccc..80c62be 100644 --- a/src/components/settings/skills-settings.tsx +++ b/src/components/settings/skills-settings.tsx @@ -990,6 +990,15 @@ export function SkillsSettings() { > {skill.scope} + {skill.read_only && ( + + {t("systemBadge")} + + )}
{skill.path} @@ -1010,6 +1019,7 @@ export function SkillsSettings() { {t("actions.preview")} { handleEditSkill(skill).catch((err) => { console.error( @@ -1034,7 +1044,12 @@ export function SkillsSettings() { {t("actions.openInWindow")} { handleRequestDeleteSkill(skill) }} @@ -1097,10 +1112,19 @@ export function SkillsSettings() { selectedSkillId || isDrafting ? (
-
+

{skillDraftId.trim() || t("newSkillTitle")}

+ {selectedSkill?.read_only && ( + + {t("systemBadge")} + + )}
@@ -1123,7 +1147,11 @@ export function SkillsSettings() { ) }) }} - disabled={skillSaving || skillReading} + disabled={ + skillSaving || + skillReading || + Boolean(selectedSkill?.read_only) + } > {skillSaving ? ( <> @@ -1189,7 +1217,10 @@ export function SkillsSettings() { onClick={() => { setIsContentEditing((prev) => !prev) }} - disabled={skillReading} + disabled={ + skillReading || + Boolean(selectedSkill?.read_only) + } > {isContentEditing ? ( <> diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 7e9a99d..e1339b8 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -302,6 +302,8 @@ "loadingSkill": "جارٍ تحميل Skill...", "emptyNoAgents": "لا يوجد وكيل متاح.", "noSelectionHint": "اختر Skill من اليسار، أو انقر على \"Skill جديد\" لإنشاء واحد.", + "systemBadge": "نظام", + "systemHint": "Skill مدمج في CLI · للقراءة فقط", "scope": { "global": "عام", "folder": "مجلد", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 0a75548..143ca3f 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -302,6 +302,8 @@ "loadingSkill": "Skill wird geladen...", "emptyNoAgents": "Kein verfügbarer Agent.", "noSelectionHint": "Wählen Sie links einen Skill oder klicken Sie auf „Neuer Skill“, um einen zu erstellen.", + "systemBadge": "System", + "systemHint": "Integrierter CLI-Skill · schreibgeschützt", "scope": { "global": "Global", "folder": "Ordner", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 2d7d066..2d81d01 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -302,6 +302,8 @@ "loadingSkill": "Loading skill...", "emptyNoAgents": "No available agent.", "noSelectionHint": "Select a skill on the left, or click \"New Skill\" to create one.", + "systemBadge": "System", + "systemHint": "Built-in CLI skill · read-only", "scope": { "global": "Global", "folder": "Folder", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 0f49203..a8720f1 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -302,6 +302,8 @@ "loadingSkill": "Cargando Skill...", "emptyNoAgents": "No hay agentes disponibles.", "noSelectionHint": "Selecciona un Skill a la izquierda o haz clic en \"Nuevo Skill\" para crear uno.", + "systemBadge": "Sistema", + "systemHint": "Skill integrado del CLI · solo lectura", "scope": { "global": "Global", "folder": "Carpeta", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 152160e..2256cbb 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -302,6 +302,8 @@ "loadingSkill": "Chargement de la Skill...", "emptyNoAgents": "Aucun agent disponible.", "noSelectionHint": "Sélectionnez un Skill à gauche ou cliquez sur « Nouveau Skill » pour en créer un.", + "systemBadge": "Système", + "systemHint": "Skill intégré du CLI · lecture seule", "scope": { "global": "Global", "folder": "Dossier", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index e3b1731..b3a6865 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -302,6 +302,8 @@ "loadingSkill": "Skillを読み込み中...", "emptyNoAgents": "利用可能なエージェントがありません。", "noSelectionHint": "左側から Skill を選択するか、「新規 Skill」をクリックして作成してください。", + "systemBadge": "システム", + "systemHint": "CLI 組み込み Skill・読み取り専用", "scope": { "global": "グローバル", "folder": "フォルダ", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index a67a7e4..fde3867 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -302,6 +302,8 @@ "loadingSkill": "Skill 불러오는 중...", "emptyNoAgents": "사용 가능한 에이전트가 없습니다.", "noSelectionHint": "왼쪽에서 Skill을 선택하거나 \"새 Skill\"을 클릭하여 만드세요.", + "systemBadge": "시스템", + "systemHint": "CLI 내장 Skill · 읽기 전용", "scope": { "global": "전역", "folder": "폴더", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index a9aced3..89ae296 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -302,6 +302,8 @@ "loadingSkill": "Carregando Skill...", "emptyNoAgents": "Nenhum agente disponível.", "noSelectionHint": "Selecione um Skill à esquerda ou clique em \"Novo Skill\" para criar um.", + "systemBadge": "Sistema", + "systemHint": "Skill integrado do CLI · somente leitura", "scope": { "global": "Global", "folder": "Pasta", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 54b901c..464354a 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -302,6 +302,8 @@ "loadingSkill": "正在加载 Skill...", "emptyNoAgents": "暂无可用 Agent。", "noSelectionHint": "从左侧选择一个 Skill,或点击“新建 Skill”创建。", + "systemBadge": "系统", + "systemHint": "CLI 内置 Skill · 只读", "scope": { "global": "全局", "folder": "文件夹", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index b9561ca..e5df3b2 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -302,6 +302,8 @@ "loadingSkill": "正在載入 Skill...", "emptyNoAgents": "暫無可用 Agent。", "noSelectionHint": "從左側選擇一個 Skill,或點擊「新建 Skill」建立。", + "systemBadge": "系統", + "systemHint": "CLI 內建 Skill · 唯讀", "scope": { "global": "全域", "folder": "資料夾", diff --git a/src/lib/types.ts b/src/lib/types.ts index 555edf2..3e8d9d3 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -557,6 +557,7 @@ export interface AgentSkillItem { scope: AgentSkillScope layout: AgentSkillLayout path: string + read_only: boolean } export interface AgentSkillsListResult {