news 2026/9/10 5:51:50

oh-my-pi 中 manage_skill 工具的实现解析:隔离式受管技能(Managed Skill)的创建、更新与删除

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
oh-my-pi 中 manage_skill 工具的实现解析:隔离式受管技能(Managed Skill)的创建、更新与删除

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-pi

manage_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)层被注册为独立 provider:builtin.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工具名
approval"write"写类工具,走审批语义
stricttrue严格结构化输出模式
loadMode"essential"常驻顶层,不挂载到xd://子树下

注册通过工厂函数ManageSkillTool.createIf(session)完成,并注册进内置工具工厂表 tools/index.ts:

static createIf(session: ToolSession): ManageSkillTool | null { if (!session.settings.get("autolearn.enabled")) return null; return new ManageSkillTool(session.refreshSkills); }

两个要点:

  1. autolearn.enabled是唯一可用性门槛,默认false(见 settings-schema.ts 中的配置定义:"autolearn.enabled": { type: "boolean", default: false, ... },UI 标签为 “Auto-Learn (experimental)”,位于 memory 标签页)。该门控只读取配置,memory.backend无关——技能侧是独立子系统。
  2. 构造函数捕获了会话的可选refreshSkills回调(实现见 agent-session.ts,底层转发到 session-tools.ts),用于在技能变更后刷新活跃技能快照。

可见性规则:启用的顶层会话在普通显式工具列表中会自动包含该工具;子代理(subagent)不会自动发现或接收它,只有在其 requested-tools/frontmatter 列表中显式包含manage_skill时才能使用。执行是单次的(single-shot),不发出进度更新。

输入参数契约

工具入参 schema 同样定义在 manage-skill.ts:

字段类型必填说明
action"create" \| "update" \| "delete"对受管技能的变更操作
namestringkebab-case 的受管技能名(小写字母、数字、连字符)
descriptionstringcreate/update 必填单行描述,驱动技能发现(discovery)
bodystringcreate/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必须同时携带descriptionbody,否则在验证阶段(而非执行阶段)即被拒绝。

执行流程

execute方法(manage-skill.ts)的完整流程:

  1. delete 分支:调用deleteManagedSkill(name)后,若refreshSkills回调存在则刷新活跃技能,返回Deleted managed skill "<name>".details = { action: "delete", name }

  2. 防御性收窄create/update缺少descriptionbody时抛出"<action>" requires both "description" and "body".。由于 schema 的narrow已先行拦截,该分支对合法输入不可达,它的作用只是向writeManagedSkill的类型契约证明字符串必然存在。

  3. 同名遮蔽检查(仅 create):若已有活跃的用户编写技能占据了该名字,直接返回isError: true的错误结果,不写任何文件。判断依据是 skills.ts 的isNameClaimedByAuthoredSkill

    export 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 },提示文本建议选择其他名字。

  4. create/update 写入:委托给writeManagedSkill(...)(见下一节),成功后再次调用refreshSkills?.(),使交互式会话能立即发现变更。

  5. 结果content[0].text分别为Created/Updated managed skill "<name>" (managed-skills/<name>/SKILL.md).details = { action, name }

名字、描述与正文的规范化规则

写入前的所有规范化都在辅助模块 managed-skills.ts 中完成:

名字(sanitizeSkillName,L34-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根目录。该名字模式在发现时还会被isValidManagedSkillName(L50-L52)复核——手工放进目录的SKILL.md若 frontmatter 名字不符合规范,不允许未转义地渲染进系统提示词。

描述(sanitizeManagedDescription,L62-L69)

return 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-L173)body去除首尾空白后必须非空(否则抛... 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 生成(toSkillFrontmatter,L75-L82):正文里不允许自带 YAML frontmatter;writeManagedSkill用仓库的 YAML helper 生成只含规范化name和净化后description的最小 frontmatter 块,并通过parseFrontmatter往返可解析。

文件系统安全设计

writeManagedSkill(L152-L230)和deleteManagedSkill(L233-L255)是本文最值得细读的部分——它针对每一层路径组件都设了防线:

  1. 根目录检查(assertManagedRootSafe,L117-L125):对managed-skills根做lstat,若根本身是符号链接则拒绝操作。注释解释了动机:对子路径lstat会跟随中间组件,符号链接的根会让合法名字写入/删除到隔离目录之外(例如落到用户技能上)。
  2. 技能目录检查:对<root>/<name>lstat(不跟随最终组件),目录若是符号链接则抛Managed skill "<name>" resolves through a symlink; refusing to write outside the managed directory.
  3. create 的原子独占创建:先mkdir(dir, { recursive: true }),再用fs.writeFile(file, content, { flag: "wx" })——即O_CREAT|O_EXCL,文件已存在则失败(关闭 check-then-write 竞态),同时天然拒绝符号链接的SKILL.mdEEXIST被翻译为Managed skill "<name>" already exists. Use action "update" to change it.
  4. 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.
  5. 先打开后截断:update 用O_WRONLY | O_NOFOLLOW打开文件句柄(符号链接触发ELOOP时转为明确的拒绝错误),在已检查的句柄上重做一遍 stat 验证,然后truncate(0)+ 写入。这样 lstat 之后路径被换成符号链接或新硬链接目标时,写入仍指向已验证的 inode。
  6. delete 的防跟随删除:递归fs.rm前同样lstat检查目录符号链接;ENOENT翻译为Managed skill "<name>" does not exist.

并发语义:按名字串行,跨名字并行

serializeSkillMutation(L99-L109)用一张进程内Map<string, Promise>实现按名字的 Promise 链:

  • 同名变更按提交顺序串行——两个工具都是非独占的,同一轮次的并行工具批次可能对同一技能同时发起两个变更(例如一个 update 观察到 delete 进行中),串行链保证按序执行;
  • 不同名字可以并行;
  • 跨进程竞态不在保障范围内(源码注释明确 out of scope)。

输出契约与错误汇总

正常输出:

actioncontent[0].textdetails
deleteDeleted 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 目标不存在、不安全根目录/符号链接目录/符号链接文件/非普通文件/多硬链接文件。

发现集成与遮蔽规则的边界

三条与发现层相关、容易被忽略的规则:

  1. update 不绕过用户技能优先级:如果某用户技能与受管技能同名,update会成功改写文件,但该受管技能在发现中依旧处于遮蔽状态(managed provider 优先级最低,见 builtin.ts 的注释:managed 是最低优先级 provider,其他任何来源的同名用户技能都会赢下 capability 级去重)。只有create有前置遮蔽检查。
  2. 描述是发现的入场券loadManagedSkillsrequireDescription: true扫描,无描述(或净化后为空)的受管技能不会出现在技能列表中——这正是writeManagedSkill要前置拒绝空描述的原因。
  3. 刷新即时性依赖回调:变更成功后的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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 5:50:39

magnitude:开源CLI本地大模型推理服务器深度指南

1. “magnitude”不是拼写错误&#xff0c;而是被严重低估的本地推理服务核心组件你有没有在调试一个本地大模型服务时&#xff0c;反复看到类似unable to locate the codex cli binary的报错&#xff0c;却始终找不到codex cli的安装包、GitHub 仓库或任何官方文档&#xff1f…

作者头像 李华
网站建设 2026/9/10 5:49:12

AI技能包Skill实战解析:安装、结构、设计与排错

我最近整理AI编程辅助工具链的时候&#xff0c;翻到一条安装命令&#xff0c;顺手就把它加进了本地环境里&#xff1a;npx skill add dietrichgebert/ponytail命令不长&#xff0c;但背后牵扯出来的东西挺值得聊&#xff1a;现在AI这种“技能包&#xff08;Skill&#xff09;”…

作者头像 李华
网站建设 2026/9/10 5:44:52

自然语言查询股票行情:DolphinDB MCP 让 AI 真正读懂金融数据

如果你平时会看股票&#xff0c;或者多少接触过量化投研&#xff0c;应该对“让 AI 帮你分析股票”这件事不陌生。但真正上手之后你会发现一个尴尬现象&#xff1a;让 AI 聊行情逻辑&#xff0c;它能说得头头是道&#xff1b;真要它拉出某只标的的历史日线、算个 MA5 和 MA20 有…

作者头像 李华
网站建设 2026/9/10 5:44:12

OpenMAIC 怎么部署到 Vercel?

OpenMAIC 怎么部署到 Vercel&#xff1f; 【免费下载链接】OpenMAIC Open Multi-Agent Interactive Classroom — Get an immersive, multi-agent learning experience in just one click 项目地址: https://gitcode.com/GitHub_Trending/op/OpenMAIC OpenMAIC 是一个标…

作者头像 李华
网站建设 2026/9/10 5:42:52

数字化转型:从概念到落地的完整路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华