自定义 skill 的编写:保持安全的模板
Simmetric Chat 的自定义 skill 如何把存好的 prompt 模板编译成一条被聚光、不可信的上下文条目 —— whitelist 与单遍替换规则、三个 scoping 层级、编译前的 DLP 掩码,以及 max_skills 的 freemium 定价(3 或不限)。

可以调用的指令,而不是必须信任的代码
自定义 skill 是可复用的 prompt 模板:写一次,之后在聊天里以斜杠命令反复调用。设计的关键就落在“prompt 模板”这四个字上:skill 不含 HTTP 调用、不含文件 I/O、不含条件逻辑、也不含沙箱 —— 无处可逃,因此这项功能按构造就是 air-gap-safe 的。skill 真正含有的,是一条边界真实的编译管线:以 schema 白名单约束的参数、只跑一遍的替换、编译前先做掩码,以及作为不可信数据抵达的注入。
本文从作者视角完整走一遍:模板如何编写与编译、斜杠命令的语法、三个 scoping 层级、保住编译输出诚实的特权边界,以及授权的形态(max_skills —— Community 3 个,Enterprise 不限)。
模板与其规则
模板就是带 {{param}} 占位符的普通字符串:
Translate to {{targetLang}}: {{input}}编译管线强制四条规则(钉在 schema 上):
- 白名单 —— 只有 skill 的
inputSchema.properties(加上它的默认参数)中声明的键才参与替换。未知的占位符在输出里保持字面 —— 从环境、系统上下文或任何别处都无法解析到它。 - 单遍替换 —— 只跑一遍替换。被替换进去的值即使内含
{...}标记形态也永远不会再次展开;模板注入在这层结构性死亡。 - 一致性 —— 模板中的每个占位符必须在 input-schema 里有对应项;没有就 create/patch = 一个点名的 400。
- 标记拒绝 —— 模板的静态文本(去除占位符后)不得包含跨供应商的 tool-call 标记形态;create/patch 时直接拒绝。参数值另有专门防御,见下。
斜杠命令语法
输入 / 打开 skills 面板(自动补全);选中某行会把它的 slug 插入但不发送。回车时客户端解析参数,随消息一起发出发出调用:
- 位置式:
/translate Ciao mondo—— 若干 token 归并进第一个必填字段。 - 键值式:
/translate input=Ciao—— 命名填充;引号段(input="Ciao mondo")携带空格。 - 混合与默认值:
input="Ciao" targetLang=Spanish,或必填键取声明好的默认值。
两个行为让语法值得信任而不是耍聪明:
- 永不报错规则 —— 识别不了的
/词、builtin 名字、或被禁用的行会自然滑入一条普通消息:无调用、无错误、无隐藏。 - 服务端重新验证一切 —— 未知键被忽略;每次调用时 slug 都会在服务端以同一 scoping 过滤器重新解析(IDOR 守卫)。
保留 slug 列表(聊天固定命令 + builtin skill 名字)让遮蔽不可能:自定义 skill 永远不能占用一个保留名。
请求路径:解析、掩码、编译、包裹
"/slug params" → POST chat stream { message, skillCall }
→ resolveInvocableSkill(服务端、带 scoping 过滤)
→ 编译前先 DLP 掩码(DLP 激活时扫描参数值)
→ compileTemplate(白名单替换、只跑一遍)+ 聚光包装
→ orchestrator 中的唯一一条用户级聚光上下文条目与 DLP 的先后 是承重的细节:参数在编译之前先掩码。若反过来编译,模板作者写下的文本可能给参数挡住扫描器;按此顺序,编译后的 prompt 只携带掩码后的值。(DLP 管线本身 —— 编号掩码、加密原稿、管理员 unmask —— 是另一篇的故事。)
特权边界
模板的编译输出是用户投喂的数据,不是指令。它以唯一一条聚光上下文条目进入对话 —— 带标签、计入预算、如同任何其他 tool result,并被特权测试钉死:自定义 skill 可以增加上下文,但永远不能增加工具、权限或注册表条目。转录里保留原始输入的消息;只有编译后的 prompt 作为 skill 上下文进入。
这就是“无沙箱”是特性而非限制的原因:模板的能力面 —— 它能影响的 —— 恰好是一条上下文条目,别无他物。
Scoping:三个层级,都在服务端推导
- 个人 —— 只有作者可解析(任何拥有创建权限的用户)。
- Workspace —— 凡在那个 workspace 里聊天的都可见(editor+ 创建;viewer 得到显式 403)。持久行的 user id 为 null —— 若按创建者盖章,这一行会跨 workspace 泄漏,schema 关闭了这个类别。
- 全局 —— 组织内所有人(仅 admin 可创建)。
客户端声称的 scoping 从不被信任:服务端从持久形态推导层级,并按聊天过滤解析。编辑与删除在下一次聊天后生效 —— 无 hooks、无重启。删除是软墓碑,slug 立即释放:被删 skill 的名字当场可以重新创建。
Freemium 事实:max_skills
授权限额镜像平台的其它数字门槛:Community 3 个自定义 skill,Enterprise 不限 —— census 行 F-SKILL-1,立在授权常量上:
- 只在 CREATE 时强制 —— 若限制调用会用授权理由打断聊天,这是被否决的设计;计数住在供给阶段。
- 计数按组织统计自定义行 —— builtin 与 marketplace 行从不计入。
- 超限时,标准付费墙形态会点名功能与层级 —— 而且出口是自助的:每个用户可以删除自己的 skill 释放配额,不需要管理员介入。
这最后一项属性把 freemium 合约做得诚实:限额在管理界面可见、402 能解释自己、出路不用提工单。
继续阅读
完整操作者参考 —— endpoints、表单对话框、test-preview 语义 —— 在 /zh/docs/custom-skills:
- Custom skills —— 操作指南
- 本文数据:
FACT-CENSUS.md行 F-SKILL-1(自定义 skill +max_skillsfreemium)与 F-DLP-1(此路径经过的掩码层)
Provenance:Simmetric Chat 的 docs/CUSTOM_SKILLS.md(v0.28.1,Phase 190 SKIL-01..05)、packages/shared/src/schemas/skill.schema.ts、license.ts(max_skills)—— 处于 census 固定的文档状态(SHA aca5b8ff)。
