FastGPT Agent Skill 设计解析:空白工作区约束、内置辅助生成与沙箱安全部署
【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT
本篇技术文章基于 FastGPT 的设计文档 Agent Skill 当前设计,系统讲解平台级 Skill 的完整生命周期:以“空白工作区 + Agent 辅助编辑”为核心的创建体验、skills/<skill-name>/SKILL.md的强制目录约束与最小发布校验、Pro 内置辅助生成 Skill 的 etag 幂等同步与目录隔离机制,以及运行态沙箱中版本包的权限校验、ZIP 安全解压和 entrypoint 受控执行。读完后,你将能够对照源码理解一个 Skill 从创建、发布到在 Sandbox 中被部署、扫描和提醒注入的完整链路,并在自行扩展 Skill 功能时准确定位各校验环节的实现位置。
一、总体目标:平台管运行态,用户管内容
FastGPT 的平台 Skill 采用“空白工作区 + Agent 辅助编辑”的创建体验。职责划分非常清晰:
- 平台负责:资源管理、版本管理、发布校验和运行态部署;
- 用户负责:Skill 的具体内容,在编辑工作区中自行创建;
- 创建弹窗不生成内容:不预生成默认需求文案,也不自动生成
SKILL.md。
这一设计在代码层有直接体现。新建 Skill 的初始版本由 createBlankSkillWorkspacePackage 生成,它只建立一个“工作区外壳”:
export async function createBlankSkillWorkspacePackage(): Promise<Buffer> { const zip = new JSZip(); zip.file('.gitignore', DEFAULT_GITIGNORE_CONTENT); zip.folder('skills'); return generateZipBuffer(zip); }源码注释明确说明:空目录在 ZIP 中需要显式写入,否则解压后skills/目录不会存在。也就是说,创建成功后用户拿到的是一个带默认 .gitignore(排除node_modules/、venv/、dist/、媒体大文件等)和空skills/目录的初始工作区。文档中“创建接口不接收requirements,也不根据名称或描述生成默认 Skill 内容”的约定,与这条代码路径一一对应。
创建成功后的标准流程为:
- 创建一个空白 workspace 初始版本;
- 进入 Skill 详情和编辑聊天;
- 用户自行创建
skills/<skill-name>/SKILL.md及相关文件,或使用内置辅助生成 Skill。
二、工作区约束:skills/<name>/SKILL.md是唯一的合法产物位置
用户 Skill 产物必须位于固定路径:
<workspace>/skills/<skill-name>/SKILL.md禁止把用户 Skill 写到 workspace 根目录或系统内置 Skill 目录。编辑态下 Agent 的 reminder 会明确提供当前工作目录和写入边界,引导模型只在约定位置落盘。
2.1 SKILL.md 的 frontmatter 解析要求
SKILL.md必须是带 YAML frontmatter 的 Markdown 文件。平台使用轻量解析器 parseSkillMarkdown 处理:
- frontmatter 必须由
---成对包裹,缺失时直接返回错误'SKILL.md must contain YAML frontmatter (delimited by ---)'; - 解析器只覆盖
key: value与一层嵌套对象的简单结构,所有标量值按文本保留,不做数字、布尔值或 null 的类型推断;复杂 YAML 解析失败会通过error字段返回给调用方; - 平台真正依赖的元数据只有
name和description两个字段。
目录名与 skill 名的对应关系由 getSafeSkillDirectoryName 保证:空白字符替换为中划线、仅保留中英文数字及中下划线、连续分隔符合并、首尾分隔符去除、长度截断到 50 字符,非法结果回退为skill。编辑发布时 standardizeSkillPackageBySkillMdName 还专门处理了一个错位问题:数据库里的skill.name只是产品展示名,真实的可执行 skill 目录名必须跟随SKILL.mdfrontmatter 中的name,否则下次打开编辑器会出现“展示名目录包住真实 skill”的错误结构。
2.2 发布前的最小结构校验
文档要求发布/保存版本前执行最小结构校验,实现位于 validateDeployableSkillWorkspacePackage。它对工作区 ZIP 逐条检查:
| 校验项 | 实现逻辑 | 失败报错 |
|---|---|---|
| ZIP 安全性 | 先走validateZipSafety(见 2.3 节) | Unsafe ZIP entry path: ... |
存在skills/目录 | 任一归一化后以skills/开头的条目 | Missing required directory: skills/ |
| 至少一个一级 skill 目录 | 收集skills/<dir>下含子文件的目录 | The skills/ directory must contain at least one first-level skill folder |
每个一级目录都有SKILL.md | 正则^skills\/([^/]+)\/SKILL\.md$匹配 | Each first-level skill folder under skills/ must contain SKILL.md: <缺失目录列表> |
注意源码中的一个设计取舍:该校验只做 workspace 级最小结构检查,不解析 frontmatter;而创建阶段的空白初始包(只有.gitignore+ 空skills/目录)不应调用该校验——只有用户主动发布时才要求至少存在一个可执行 Skill 目录。这与“空白工作区创建”和“发布必须可运行”两个阶段解耦的文档表述一致。
2.3 ZIP 安全校验:拒绝绝对路径、..与符号链接
validateZipSafety 是“版本包不能通过绝对路径或..逃逸工作区”这条约束的具体实现,对每个 ZIP 条目:
- 拒绝空路径与含
\0的路径; - 拒绝以
/或\开头的绝对路径,拒绝C:\风格的盘符路径; - 任一路径段等于
..即判为不安全; - 通过 Unix 权限位
0xa000识别符号链接条目,直接拒绝(ZIP symlink entries are not allowed); - 累计所有非目录条目的解压后大小,超过
maxUncompressedBytes上限时报ZIP archive uncompressed size exceeds maximum allowed size。
此外 validateZipStructure 在安全校验之上再做结构兼容:优先找根目录SKILL.MD,其次找/skills/.../SKILL.MD,最后兜底任意子目录下的SKILL.md,以兼容历史单 skill 包与多 skill 包两种形态。
三、内置辅助生成 Skill:Pro 注入、HOME 隔离与 etag 幂等同步
平台(Pro)可以提供内置的辅助生成 Skill 源码,让用户在空白工作区里用“一句话需求”生成 Skill。运行时遵循严格的边界,全部体现在源码中:
3.1 通用注入协议与目录隔离
注入协议的类型定义非常薄,BuiltinSkillSource 只描述“一个名字 + 一组relativePath → Buffer的文件”,社区版因此可以通过可选注入接口保持零依赖——不注入任何 source 时整个同步流程直接短路返回。
同步实现 syncBuiltinSkillsToSandbox 的关键行为:
- 写入位置:
<homeDirectory>/.fastgpt/skills/<name>,即 Sandbox HOME 之下,而非用户 workspace。源码注释明确指出:目标路径不在用户 workspace 内,因此不会进入编辑器文件树、导出包或发布包——这就是“内置 Skill 不进入用户版本包”的实现依据; - 先清理再写入:若目标目录已存在(目录或文件),先删除旧内容再重建,避免新旧文件混叠;
- 失败即抛错:任一文件写入失败会抛出
Failed to write builtin skill files: ...,不会留下半同步状态。
3.2 etag 幂等同步
“内容没有变化时不覆盖”由 computeBuiltinSkillEtag 实现:
- 对每个文件内容做
buildRuntimeHash,与relativePath配对; - 按
relativePath字典序排序(保证同内容必然产生同序); - 将
path:hash\n拼接后整体再哈希一次,得到整个 Skill 目录的 etag。
每个 etag 以builtinSkill:<name>为键写入沙箱 runtime state(getBuiltinSkillStateHashKey)。同步前先比较 state 中记录的 etag 与本次计算值,全部未变化时直接返回,不产生任何写盘操作;部分变化时只重写有变化的 Skill。这使得重复进入编辑会话不会反复覆盖 HOME 下的内置目录。
四、发布:不可变 ZIP 包 + 持久化前校验
Skill 版本保存为不可变 ZIP 包并记录 storage key;版本记录本身由 createVersion 写入MongoAgentSkillsVersion集合(支持传入事务 session,保证与 skill 主记录的原子性)。文档强调的“发布校验在持久化版本前执行,失败时不生成可运行版本”,对应的就是第二节所述的结构/安全校验链路:校验不过则 ZIP 不会进入对象存储、版本记录不会落库,运行态也就无从注入一个损坏的包。
五、普通 Agent 运行:从选中 Skill 到 reminder 注入
文档列出的五步运行流程,在 injectAgentSkillFilesToSandbox 和 getAgentSkillInfos 中有完整实现。
5.1 权限校验:团队归属 + 成员读权限
注入函数先按skillIds + teamId + deleteTime: null查询MongoAgentSkills,确认 Skill 属于当前团队且未被删除;随后对每个 Skill 调用 authSkillByTmbId(per: ReadPermissionVal)校验当前成员读权限。无权限或已不存在的 Skill 会被静默跳过(打 warn 日志),而不是让整个运行失败——这保证了“校验团队和成员读取权限”不会放大为可用性故障。
5.2 以 versionId 为部署目录,避免同名覆盖
“已发布 Skill 以 versionId 作为部署目录”的落地方式:
- 部署根目录为
<workDirectory>/projects,每个版本解压到<projectsRoot>/<versionId>(versionId是 24 位十六进制 Mongo ObjectId); - isSafeDirectSkillVersionDir 用
/^[a-fA-F0-9]{24}$/严格识别合法版本目录,注入结束时清理所有不在期望集合内的旧版本目录(stale cleanup),并顺带删除崩溃残留的.tmp-*临时目录; - 由于同一个 versionId 的内容不可变,部署是幂等的:目标目录已存在则跳过下载。
5.3 沙箱内的 ZIP 双重安全检查
ZIP 包从对象存储以流式方式写入临时目录后,在沙箱容器内执行一条统一的解压命令(core.ts 中 unzipCommand):
unzip -Z -t package.zip | awk -v max=<maxPackageBytes> 'BEGIN { ok=0 } /uncompressed,/ { ok=(($3 + 0) <= max) } END { exit ok ? 0 : 1 }' && unzip -Z1 package.zip | awk 'BEGIN { ok=1 } /^\/\// || /(^|\/)\.\.($|\/)/ { ok=0 } END { exit ok ? 0 : 1 }' && unzip -o -q package.zip -d <tempDir>三段&&短路执行:第一段用unzip -Z -t解析清单,在“uncompressed”汇总行的未压缩总字节数超过getAgentSandboxSkillMaxBytes()上限时退出码非零;第二段用unzip -Z1逐行检查条目名,出现绝对路径/...或..路径段即失败;全部通过才执行真正的unzip -o -q。解压成功后删除 ZIP、将临时目录moveFiles到版本目录,任何一步失败都会回滚清理临时目录。Skill 编辑会话的 prepare 链路(deploySkillPackage)复用同一套三段式检查命令,并在工作区缺少.gitignore时写入默认内容。
5.4 entrypoint.sh:可选的版本初始化脚本
“可选执行版本根目录的entrypoint.sh”由 runAgentSkillVersionEntrypoints 实现:
- 仅当版本目录根下存在
entrypoint.sh文件时才执行(getFileInfo探测); - 命令通过
buildLimitedOutputShellCommand包裹为/bin/bash entrypoint.sh,以限制输出规模,工作目录为该版本目录——即“entrypoint 在隔离 Sandbox 中执行,限制时间和输出”; - 执行成功标记只记录
versionId到沙箱 runtime state 的skillEntrypoints列表(entrypoint.ts#L49-L55)。源码注释解释了依据:版本包以 versionId 为目录且内容不可变,因此无需再记录脚本 hash;同一版本已执行过就跳过,失败(result为假值)则不写入成功标记,下一轮可重试,符合“失败不标记为成功”; - 每次执行后同步裁剪 state 列表,只保留当前选中的 versionId,防止列表无限增长。
5.5 扫描 SKILL.md 并注入当前轮 reminder
getAgentSkillInfos 负责“扫描SKILL.md并把可用 Skill 信息注入当前轮 reminder”:
- 扫描命令为
find <dir> \( -name node_modules -o -name .venv -o -name venv \) -prune -o -iname "SKILL.md" -print0,多目录并发 find,单个目录失败只 warn 不级联; - 对找到的每个
SKILL.md调用parseSkillMarkdown,没有合法name的条目被丢弃(frontmatter 不满足平台解析要求的文件不会进入 reminder); - 扫描范围同时覆盖用户 workspace 与内置 Skill 目录(HOME 下的
.fastgpt/skills),普通运行与 edit-debug 统一以沙箱工作区为准; - 输出结构包含
name、description、directory、skillMdPath,并关联已发布版本的appId/appName/appDescription,供 reminder 拼装。
文档中“模型必须先读取匹配 Skill 的完整SKILL.md,不能仅凭描述推断工作流”是 reminder 文案层的约束:注入的只是名称、描述和路径,模型被要求在真正使用某个 Skill 前读取该路径下的完整文件,避免用一句话描述代替完整工作流指令。
六、Skill Edit Debug:直接复用编辑沙箱工作区
编辑调试(edit-debug)不下载已发布版本,而是直接使用 Skill Edit Sandbox 的当前工作区做 SKILL.md 扫描——getAgentSkillInfos的注释明确区分了这两条路径:“普通运行先注入 skill 包,edit-debug 复用编辑器正在运行的 sandbox,然后统一扫描 SKILL.md”。这避免了用旧发布版本覆盖用户正在编辑内容的风险;而内置辅助生成 Skill 依然位于 Sandbox HOME,与用户工作区保持隔离,两条路径互不干扰。
七、安全边界汇总
将分散在各实现中的安全控制汇总如下,每条都可回溯到具体源码:
| 安全约束 | 实现位置 |
|---|---|
| ZIP 解压前校验总大小与路径穿越 | 构建侧 validateZipSafety;运行侧 core.ts 的 awk 三段式检查 |
| 部署前检查团队归属与成员读权限 | injectAgentSkillFilesToSandbox |
| 内置 Skill 不进入用户版本包 | 写入 HOME 下.fastgpt/skills,见 builtin.ts |
| entrypoint 隔离执行、限制输出、失败不标记成功 | runAgentSkillVersionEntrypoints |
| versionId 作为部署目录避免同名覆盖 | isSafeDirectSkillVersionDir 及 stale 目录清理 |
版本目录仅接受 24 位十六进制 ID,临时目录仅接受.tmp-*前缀 | 同上两个正则识别函数 |
八、验证范围
文档给出的回归验证清单,可作为改动该模块时的测试基线:
- 空白 workspace 创建和创建接口 schema(不接收
requirements); - 最小发布结构(
skills/存在、一级目录必须有SKILL.md)与非法路径校验(绝对路径、..、符号链接); - Skill 包权限(团队归属 + 读权限)、大小限制(
maxUncompressedBytes)和部署目录(versionId 命名、stale 清理); - edit-debug 不覆盖当前工作区(不注入旧发布版本);
- 内置 Skill 路径隔离(HOME 下
.fastgpt/skills)、etag 幂等同步(内容不变不写盘)与扫描(workspace + 内置目录双目录 find); - entrypoint 的成功、失败和重复执行行为(成功标记按 versionId 去重、失败可重试、state 列表裁剪)。
相关核心文件索引:通用注入协议 packages/global/core/ai/skill/runtime/builtin.ts、沙箱同步 packages/service/core/ai/sandbox/application/runtime/skill/builtin.ts、运行态扫描与部署 packages/service/core/ai/sandbox/application/runtime/skill/core.ts、ZIP 构建与校验 packages/service/core/ai/skill/package/zipBuilder.ts、SKILL.md 解析工具 packages/service/core/ai/skill/utils.ts、默认 .gitignore 模板 packages/service/core/ai/skill/package/constants.ts。
【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考