Skill 运行时配置机制

借鉴 GitHub Actions 的分层变量模型:四个层级逐层覆盖,在 Skill 被加载时注入成环境变量。

问题

一个 Skill 想跑起来,需要一批"配置"——比如工作目录、数据库地址、数据库密码、要查哪张表。 问题是没有一个人能知道全部:平台知道环境,开发者知道连哪些资源,用户知道这次要干嘛。 还有个特殊要求:开发者知道密码,想让 Skill 调工具时能用,但不想让用户和智能体看到明文

借鉴 GitHub:分层变量 + 逐层覆盖

GitHub Actions 早就解决过同一个问题。它不给变量打"来源"标签,而是分成几个层级, 同名变量越靠近具体使用场景的层级,优先级越高。我们照搬这个思路。

GitHub Actions 的做法

  • Organization 变量(最通用)
  • Repository 变量(覆盖 org)
  • Environment 变量(最具体,覆盖前两层)
  • Variables 明文 / Secrets 打码,分开存

我们的四个层级

  • 平台层(最通用)
  • 组织 / org 层(覆盖平台)
  • Skill 自己层(覆盖 org)
  • 用户配置层(最具体,覆盖前三层)

四个层级怎么覆盖

每一层都可以为同一个配置项提供值。下面的层覆盖上面的层, 谁离用户最近、谁说了算。

① 平台 系统自动提供环境信息,所有 Skill 通用的兜底值 WORKSPACE_DIR
▼ 被下层覆盖 ▼
② 组织 / org 同一个组织下多个 Skill 共享的值(如组织统一的数据库) DB_HOST
▼ 被下层覆盖 ▼
③ Skill 自己 Skill 开发者发布时填的、这个 Skill 专属的值 DB_PASSWORD(密钥)
▼ 被下层覆盖 ▼
④ 用户配置 用户在使用前 / 使用中提供的值,优先级最高 TARGET_TABLE
最终值 = 从上往下合并,下层赢
不需要"声明谁来填"。 每一层都可以给任意配置项赋值,机制完全一样。 某个值由哪一层提供、被哪一层覆盖,是运行时合并出来的,不用提前指定。
同一层内不会冲突。 每一层就是一张"变量名 → 值"的表,一个名字在一层里只有一个值。 所以同一个 org 下的两个 Skill 声明了相同的变量名——它们在 org 层读到的就是同一个值, 这正是"org 层共享"想要的效果。如果这两个 Skill 偏偏需要不同的值, 就各自在更具体的 Skill 层把它覆盖掉(Skill 层是按单个 Skill 隔离存的,互不影响)。

关键边界:两个 Skill 同名变量、值却不同

举个会让人担心的例子:「获取商品」声明了 DB_URL「获取用户」也声明了 DB_URL,但两个 Skill 要连的是不同的库。 用户说"获取某用户的全部商品",智能体先加载 user skill、再加载 product skill—— 看起来 DB_URL 就撞了。

实际上不会撞,因为环境变量不是注入到一个全局共享的环境, 而是「执行某个 Skill 的工具时,只注入那个 Skill 自己的那份配置」(见流程第 4 步):

# 调 product skill 的工具时: # 调 user skill 的工具时: DB_URL=商品库 DB_URL=用户库

两个 DB_URL 永远不会同时出现在同一个环境里。 变量名只要求"在单个 Skill 内唯一",跨 Skill 天然隔离。

这给实现定了一条硬约束:注入必须按每一次工具执行隔离绝不能写全局 os.environ。 子进程工具/命令/MCP 用传 env= 的方式各传各的; 进程内运行的工具不能依赖真实的全局环境变量,要用上下文传参拿到本 Skill 的配置。 只要守住这条,"同名不同值"就不是漏洞,而是被设计正常处理掉的情况。

怎么保证两次执行的环境真的不一样

上面说"调 product 的工具时 DB_URL=A,调 user 的工具时 DB_URL=B"——凭什么能保证? 关键是:执行环境不是某个会被改来改去的"当前状态", 而是每次调用时,从"这个工具属于哪个 Skill"现场算出来的。靠两件事:

1
工具和 Skill 的归属,在加载时就定死了

blade-agent 加载一个 Skill 时,它的工具注册进 ToolRegistry 就带上了 skill_id 命名空间(这个绑定代码里已经有)。 一个工具属于哪个 Skill,是加载那一刻定死的事实,之后不会变。

2
每次调用,env 是现场从 skill_id 推出来的

工具调用的分发逻辑走一条纯推导链,每次调用独立跑一遍:

拿到要调的工具 → 查它的 skill_id # 注册时就带着 → 取这个 skill 的配置表 # 加载时按 skill_id 合并好、存好 → 现场拼出这一次执行的 env → 把 env 传给这一次执行

调 product 的工具

skill_id = product → product 配置表 → DB_URL=商品库

调 user 的工具

skill_id = user → user 配置表 → DB_URL=用户库
为什么不会错:因为根本不存在一个"全局当前环境"被先后改写—— 也就没有"上一次的值残留到下一次"的问题。 每次调用都是 工具 → skill_id → 配置表 → env 这条链独立算一遍。"保证不一样"不靠时序上的小心, 而靠这条不可变的推导链:env 由 skill_id 决定,skill_id 由工具决定, 工具归属在加载时定死。

完整流程

1
Skill 声明它需要哪些配置项

在 SKILL.md 顶部的 frontmatter 里,只列出名字,并标出哪些是密钥。 不写值、不写来源。

config: - WORKSPACE_DIR - DB_HOST - DB_PASSWORD secret: true - TARGET_TABLE
2
智能体在运行过程中动态选择并加载某个 Skill

注意:Skill 不是 session 一开始就定好的。智能体是在 session 运行途中 根据任务挑选 Skill 并加载的。所以配置的合并和注入,发生在每次加载 Skill 的那一刻, 不是 session 启动时。

3
加载这个 Skill 时,针对它合并四层配置

平台 → org → Skill → 用户,按覆盖规则合并出这一个 Skill 的最终值。 谁有值谁填,下层盖上层。不做"是否填全"的预先检查。 不同 Skill 各自合并,互不影响。

4
执行这个 Skill 的工具 / 命令 / MCP 时,把合并结果作为环境变量注入

合并好的值绑定在这个 Skill 上。之后凡是执行它的工具、命令、连它的 MCP, 就把这些值注入到那次执行的环境变量里。这是核心动作。

# 执行该 Skill 的工具时,环境变量里: WORKSPACE_DIR=/workspace DB_HOST=10.x.x.x DB_PASSWORD=•••••••• TARGET_TABLE=orders_2026
5
Skill 的代码 / 工具 / MCP 用 os.environ

需要哪个配置就读哪个。智能体只管调工具,不碰这些值。

没填全怎么办?不用管。 如果某个配置项四层都没人提供,不要去检查、也不要提前告诉模型。 直接跑就行——如果模型真的用到了,工具自然会报错,模型看到报错会自己去问用户。 省掉一整套校验逻辑。

密钥为什么自动就安全了

关键点:智能体(大模型)根本不读环境变量。环境变量是给 Skill 代码用的, 不进入和大模型的对话。密码进了环境变量这一步,天然就和大模型隔开了。

✅ 能用到密码的

  • Skill 的工具代码 读 env
  • Skill 连接的数据库 / MCP 读 env

🚫 看不到密码明文的

  • 智能体 / 大模型 不读 env
  • 业务用户 界面不显示
  • SKILL.md 文件本身 只有名字声明
唯一要补的小动作:工具结果回给智能体之前做一次打码—— 万一某个工具把密码原样回显,自动替换成 ••••, 防止密码顺着工具结果漏进对话。

两个项目怎么分工

这套机制要同时改 skill_registry(Skill 注册中心)和 blade-agent(智能体本体)。 四个层级的值分别住在两个项目里。

层级 值住在哪 怎么来的
平台 blade-agent 运行环境自带
org skill_registry org 是 registry 的概念,多个 Skill 共享
Skill skill_registry 开发者发布 Skill 时填,跟着 Skill 走
用户 blade-agent 智能体在 session 里追问出来的

skill_registry 要改

  • parser 解析 SKILL.md 的 config: 声明段(现在只读 name/description)
  • 新增 org 层 / Skill 层配置值的存储;密钥单独加密存
  • 这些值不写进 SKILL.md 正文、不打包进下载 zip(否则会展示给大模型)
  • 扩展已有的"取 Skill 内容"接口:响应里额外带上 org 层 + Skill 层的值(含密钥)

blade-agent 要改

  • frontmatter parser 也解析 config: 声明
  • 平台层值从运行环境取;用户层值由智能体追问后存进 session
  • 加载 Skill 时把四层合并出最终值
  • 执行该 Skill 的工具 / 命令 / MCP 时注入环境变量
  • 工具输出打码
不用新开连接。 blade-agent 运行时本来就通过 HTTP 连 skill_registry—— 搜索、Get Skill Content(下载并读取)。org 层 / Skill 层的值跟着 Skill 内容一起回来, 复用这条已有的通道。密钥安全的边界是:它出现在 API 响应里(发给 blade-agent 运行时), 但不出现在 SKILL.md 正文里(发给大模型)。

这版方案刻意不做什么