的创建、更新与删除)
oh-my-pi 中 manage_skill 工具的实现解析隔离式受管技能Managed Skill的创建、更新与删除【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pimanage_skill是 oh-my-pi一个内嵌 IDE 能力的编码智能体中用于直接维护“受管技能”managed skill的内置工具它把可复用的操作流程如某类问题的调试步骤、项目专属工作流固化为独立目录下的SKILL.md文件供后续会话像普通技能一样被发现和注入。读完本文你将掌握它的启用条件、三个 action 的完整行为契约、名字/描述/正文的校验规则以及源码层面防符号链接逃逸、防硬链接篡改和并发串行化的安全设计。工具定位与用户技能authored skill彻底隔离oh-my-pi 的技能体系区分两类来源用户编写技能authored skills存放在~/.omp/agent/skills等项目或用户技能目录中由人编写和维护受管技能managed skills由manage_skill工具以及自动学习相关的learn工具自动生成的技能文件统一存放在隔离目录~/.omp/agent/managed-skills下。核心隔离原则在辅助模块 managed-skills.ts 的头部注释中写得很明确该目录下所有写入都被限制在getManagedSkillsDir()之内——“自动管理永远不能触碰用户编写的技能”。对应地manage_skill的模型提示词manage-skill.md也反复强调“User-authored skills separate; tool NEVER edits them”。从源码结构看受管技能在发现discovery层被注册为独立 providerbuiltin.ts 中以MANAGED_SKILLS_PROVIDER_ID omp-managed注册了一个优先级最低MANAGED_SKILLS_PRIORITY 5的 provider无条件扫描managed-skills目录空目录扫描是 no-op。这意味着发现的门是常开的只有写入manage_skill和自动学习提醒auto-learn 的其他子功能受autolearn.enabled门控。启用条件与注册可见性工具元数据定义在 manage-skill.ts属性值含义namemanage_skill工具名approvalwrite写类工具走审批语义stricttrue严格结构化输出模式loadModeessential常驻顶层不挂载到xd://子树下注册通过工厂函数ManageSkillTool.createIf(session)完成并注册进内置工具工厂表 tools/index.tsstatic createIf(session: ToolSession): ManageSkillTool | null { if (!session.settings.get(autolearn.enabled)) return null; return new ManageSkillTool(session.refreshSkills); }两个要点autolearn.enabled是唯一可用性门槛默认false见 settings-schema.ts 中的配置定义autolearn.enabled: { type: boolean, default: false, ... }UI 标签为 “Auto-Learn (experimental)”位于 memory 标签页。该门控只读取配置与memory.backend无关——技能侧是独立子系统。构造函数捕获了会话的可选refreshSkills回调实现见 agent-session.ts底层转发到 session-tools.ts用于在技能变更后刷新活跃技能快照。可见性规则启用的顶层会话在普通显式工具列表中会自动包含该工具子代理subagent不会自动发现或接收它只有在其 requested-tools/frontmatter 列表中显式包含manage_skill时才能使用。执行是单次的single-shot不发出进度更新。输入参数契约工具入参 schema 同样定义在 manage-skill.ts字段类型必填说明actioncreate \| update \| delete是对受管技能的变更操作namestring是kebab-case 的受管技能名小写字母、数字、连字符descriptionstringcreate/update 必填单行描述驱动技能发现discoverybodystringcreate/update 必填SKILL.md的 Markdown 正文不得包含 frontmatter跨字段约束不是用“判别联合”实现的而是 schema 的narrow子句(p, ctx) p.action delete || (p.description ! undefined p.body ! undefined) || ctx.mustBe(used with both description and body for create and update),源码注释解释了选择原因保持单一根对象结构而非 discriminated union因为 strict 结构化输出模式和 Anthropic 工具 schema 生成器都要求 wire schema 是单个根对象。因此delete只需name而create/update必须同时携带description与body否则在验证阶段而非执行阶段即被拒绝。执行流程execute方法manage-skill.ts的完整流程delete 分支调用deleteManagedSkill(name)后若refreshSkills回调存在则刷新活跃技能返回Deleted managed skill name.及details { action: delete, name }。防御性收窄create/update缺少description或body时抛出action requires both description and body.。由于 schema 的narrow已先行拦截该分支对合法输入不可达它的作用只是向writeManagedSkill的类型契约证明字符串必然存在。同名遮蔽检查仅 create若已有活跃的用户编写技能占据了该名字直接返回isError: true的错误结果不写任何文件。判断依据是 skills.ts 的isNameClaimedByAuthoredSkillexport function isNameClaimedByAuthoredSkill(name: string): boolean { return getActiveSkills().some( skill skill.name name skill._source?.provider ! MANAGED_SKILLS_PROVIDER_ID, ); }之所以“预先拒绝”而不是写完再说是因为受管技能在发现时优先级最低同名用户技能永远胜出——写一个被遮蔽的文件只会报告虚假的 Created。错误结果携带details { action: create, name, shadowed: true }提示文本建议选择其他名字。create/update 写入委托给writeManagedSkill(...)见下一节成功后再次调用refreshSkills?.()使交互式会话能立即发现变更。结果content[0].text分别为Created/Updated managed skill name (managed-skills/name/SKILL.md).details { action, name }。名字、描述与正文的规范化规则写入前的所有规范化都在辅助模块 managed-skills.ts 中完成名字sanitizeSkillNameL34-L42先trim().toLowerCase()再必须匹配/^[a-z0-9][a-z0-9-]{0,63}$/——1 到 64 字符小写字母/数字开头仅允许小写字母、数字和连字符。任何不符合的名字抛出Invalid skill name raw. Use lowercase letters, digits, and hyphens (1-64 chars, starting with a letter or digit).。严格白名单同时天然阻断了..、路径分隔符和大小写变体使非法名字永远无法逃逸出managed-skills根目录。该名字模式在发现时还会被isValidManagedSkillNameL50-L52复核——手工放进目录的SKILL.md若 frontmatter 名字不符合规范不允许未转义地渲染进系统提示词。描述sanitizeManagedDescriptionL62-L69return raw .replace(/[\p{Cc}\p{Cf}]/gu, ) // 控制字符与格式字符 → 空格 .replace(/[]/g, ) // 尖括号与反引号 → 删除 .replace(/~{2,}/g, ~) // 连续波浪线折叠为单个 ~ .replace(/\s/g, ) // 空白折叠为单行 .trim();源码注释把这里明确称为信任边界受管描述由历史任务内容生成且跨会话持久化必须剥离能“跳出”系统提示词skills列表的内容——控制/格式字符、system-directive//skills之类的尖括号、Markdown 围栏分隔符反引号、~~~。该净化在写入与读取两侧都应用已存在的文件也保持安全。净化后为空则抛Managed skill name needs a non-empty description.否则发现扫描因requireDescription: true会静默丢弃该技能工具就会为“从未出现的技能”报告成功。正文与大小上限L155-L173body去除首尾空白后必须非空否则抛... needs a non-empty body.。最终文件 生成的 frontmatter 正文其UTF-8 字节长度上限为MAX_MANAGED_SKILL_BYTES 64_000注释强调限的是最终文件字节数而不是 body 的 UTF-16 长度超限抛Managed skill is bytes bytes; the limit is 64000. Trim the body or description.。frontmatter 生成toSkillFrontmatterL75-L82正文里不允许自带 YAML frontmatterwriteManagedSkill用仓库的 YAML helper 生成只含规范化name和净化后description的最小 frontmatter 块并通过parseFrontmatter往返可解析。文件系统安全设计writeManagedSkillL152-L230和deleteManagedSkillL233-L255是本文最值得细读的部分——它针对每一层路径组件都设了防线根目录检查assertManagedRootSafeL117-L125对managed-skills根做lstat若根本身是符号链接则拒绝操作。注释解释了动机对子路径lstat会跟随中间组件符号链接的根会让合法名字写入/删除到隔离目录之外例如落到用户技能上。技能目录检查对root/name做lstat不跟随最终组件目录若是符号链接则抛Managed skill name resolves through a symlink; refusing to write outside the managed directory.。create 的原子独占创建先mkdir(dir, { recursive: true })再用fs.writeFile(file, content, { flag: wx })——即O_CREAT|O_EXCL文件已存在则失败关闭 check-then-write 竞态同时天然拒绝符号链接的SKILL.md。EEXIST被翻译为Managed skill name already exists. Use action update to change it.update 的多重验证文件必须已存在否则Managed skill name does not exist. Use action create to add it.、不是符号链接、是普通文件isFile()、且nlink 1——nlink 1意味着该 inode 可能通过硬链接与用户文件共享抛... has n hard links; refusing to overwrite a file that may be user-authored elsewhere.先打开后截断update 用O_WRONLY | O_NOFOLLOW打开文件句柄符号链接触发ELOOP时转为明确的拒绝错误在已检查的句柄上重做一遍 stat 验证然后truncate(0) 写入。这样 lstat 之后路径被换成符号链接或新硬链接目标时写入仍指向已验证的 inode。delete 的防跟随删除递归fs.rm前同样lstat检查目录符号链接ENOENT翻译为Managed skill name does not exist.并发语义按名字串行跨名字并行serializeSkillMutationL99-L109用一张进程内Mapstring, Promise实现按名字的 Promise 链同名变更按提交顺序串行——两个工具都是非独占的同一轮次的并行工具批次可能对同一技能同时发起两个变更例如一个 update 观察到 delete 进行中串行链保证按序执行不同名字可以并行跨进程竞态不在保障范围内源码注释明确 out of scope。输出契约与错误汇总正常输出actioncontent[0].textdetailsdeleteDeleted managed skill name.{ action: delete, name }createCreated managed skill name (managed-skills/name/SKILL.md).{ action: create, name }updateUpdated managed skill name (managed-skills/name/SKILL.md).{ action: update, name }create 遇到同名用户技能Cannot create managed skill ... an authored skill of that name already exists ...{ action: create, name, shadowed: true }且isError: true无文件写入异常路径均抛错或被 schema 拒绝非法名字、create/update 缺字段、净化后描述为空、正文为空、超过 64000 字节、create 目标已存在、update/delete 目标不存在、不安全根目录/符号链接目录/符号链接文件/非普通文件/多硬链接文件。发现集成与遮蔽规则的边界三条与发现层相关、容易被忽略的规则update 不绕过用户技能优先级如果某用户技能与受管技能同名update会成功改写文件但该受管技能在发现中依旧处于遮蔽状态managed provider 优先级最低见 builtin.ts 的注释managed 是最低优先级 provider其他任何来源的同名用户技能都会赢下 capability 级去重。只有create有前置遮蔽检查。描述是发现的入场券loadManagedSkills以requireDescription: true扫描无描述或净化后为空的受管技能不会出现在技能列表中——这正是writeManagedSkill要前置拒绝空描述的原因。刷新即时性依赖回调变更成功后的refreshSkills()让交互式会话TUI、RPC 等模式均有对应调用点如 interactive-mode.ts、rpc-mode.ts立即看到新技能无回调的运行环境如纯子代理调用则从下一个新会话开始生效。适用前提与关联文件适用前提oh-my-pi 当前仓库版本启用manage_skill需将autolearn.enabled设为true默认关闭零足迹它与内存后端mnemopi/sharpshooter 等相互独立autolearn.autoContinue默认false控制停止时自动跑一个私密捕获轮次与autolearn.minToolCalls默认 5配置文件专属旋钮是同组下的其他开关但不影响本工具的注册。核心实现packages/coding-agent/src/tools/manage-skill.ts、packages/coding-agent/src/autolearn/managed-skills.ts模型提示词packages/coding-agent/src/prompts/tools/manage-skill.md技能发现与同名判定packages/coding-agent/src/extensibility/skills.ts、packages/coding-agent/src/discovery/builtin.ts工具注册表与工厂packages/coding-agent/src/tools/index.ts配置定义packages/coding-agent/src/config/settings-schema.ts测试覆盖test/autolearn-managed-skills.test.ts、test/autolearn-tools-gating.test.ts、test/autolearn-discovery.test.ts相关工具learn工具tools/learn.ts复用同一套writeManagedSkill/sanitizeSkillName原语与同样的同名遮蔽检查属于自动学习特性在任务结束时的“顺带固化”路径与manage_skill的显式直接变更互为补充。【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考