1. SecurityClaw 学习笔记/

08 技能系统的 manifest 契约机制

结论先说:manifest 的价值不在“配置了什么”,而在“框架可以据此拒绝什么” #

很多项目里的 manifest.yaml 最后都会退化成一张说明书:

  • 叫什么
  • 版本号是多少
  • 作者是谁

这类 manifest 对运行时几乎没有约束力。SecurityClaw 这套技能系统里,manifest 更像可执行契约:它不只是描述技能,而是给框架提供拒绝、跳过、调度和隔离的依据。


一、manifest 在系统里的职责 #

你可以先把 manifest 抽象成四类信息:

class SkillManifest(BaseModel):
    name: str
    version: str

    # 1. 调度信息
    schedule_interval_seconds: Optional[int]
    schedule_cron_expr: Optional[str]
    run_on_first_startup: bool = False

    # 2. 依赖信息
    required_env_vars: list[str] = []
    depends_on: list[str] = []

    # 3. 能力约束
    allowed_actions: list[str] = []
    allowed_field_patterns: list[str] = []
    risk_level: Literal["low", "medium", "high"] = "low"

    # 4. 运行属性
    idempotent: bool = False

这四类信息分别解决不同问题:

  1. 调度:什么时候运行
  2. 依赖:缺了什么不能运行
  3. 约束:允许做什么、不允许做什么
  4. 运行时属性:重试、风险、幂等性如何处理

二、为什么 manifest 是“契约”,不是“配置表” #

配置表的思路是:

  • 写一点元数据
  • 系统尽量照着跑

契约的思路是:

  • 你声明一组条件
  • 系统在条件不满足时明确拒绝或跳过

也就是说,manifest 的价值不在“帮助系统更方便运行”,而在“帮助系统知道什么时候不该运行”。

例子 1:环境变量缺失 #

if env_var not in os.environ:
    return InvalidManifest("ENV_MISSING")

这一步不是为了友好提示,而是为了避免技能在真正执行时才因为缺凭证崩掉。

例子 2:依赖环 #

# A depends_on B, B depends_on A
raise InvalidDependencyChain("cycle detected")

如果系统在启动期就不能看出依赖环,那运行期迟早会在调度里死锁或反复跳转。

例子 3:CRON / interval 冲突 #

如果一个技能同时声明:

  • schedule_interval_seconds
  • schedule_cron_expr

框架必须有明确规则:

  • 选一个
  • 或直接拒绝

不能含糊执行。

这就是契约思维:不确定性必须在系统边界被收敛。


三、manifest 验证流程其实就是一层静态防火墙 #

sequenceDiagram
    participant Loader as SkillLoader
    participant FS as FileSystem
    participant Manifest as Validator
    participant Registry as Runner

    Loader->>FS: 读取 skills/<name>/manifest.yaml
    FS-->>Loader: 原始 YAML
    Loader->>Manifest: validate_manifest(raw)
    Manifest->>Manifest: 校验字段类型 / 依赖 / env / 调度冲突
    alt 校验通过
        Manifest-->>Loader: SkillManifest
        Loader->>Registry: 注册技能
    else 校验失败
        Manifest-->>Loader: InvalidManifest
        Loader->>Loader: 记录 warning 并跳过
    end

这层验证很像编译期静态检查:

  • 技能还没运行
  • 业务逻辑还没执行
  • 但系统已经能拒绝明显不合法的能力包

如果没有这层,所有错误都会拖到运行期才暴露,代价会高很多。


四、最关键的三类失败路径 #

4.1 依赖失败 #

场景 #

技能声明:

required_env_vars:
  - DB_PASSWORD

但进程环境里没有 DB_PASSWORD

正确行为 #

  • 技能不注册
  • 系统继续启动
  • 输出结构化 warning

为什么不能直接炸启动 #

因为一个技能坏了,不应该拖垮整套系统。


4.2 调度失败 #

场景 #

cron 表达式非法,或者 interval 类型错误。

正确行为 #

  • 该技能拒绝注册自动调度
  • 但如果核心逻辑合法,是否还能保留手动调用能力,要有明确策略

这里就有 tradeoff:

方案 A:调度字段出错就整个技能跳过 #

优点:简单 缺点:一个小调度错误就让技能彻底不可用

方案 B:自动调度禁用,但允许手动调用 #

优点:更宽容 缺点:运行语义更复杂

如果是我,我更倾向 B。因为“不能自动跑”和“不能被调用”不是一回事。


4.3 约束失败 #

场景 #

manifest 声明:

allowed_actions: [read]

但 LLM 规划里出现了 delete

正确行为 #

运行时直接拒绝,不进入执行器。

这说明 manifest 的价值不仅在启动期,也在运行期持续生效。它是控制面的输入之一。


五、为什么文件系统扫描是合理但不完美的选择 #

这套系统允许把技能目录直接放进 skills/,Loader 扫描后自动注册。

这个设计的优点 #

  1. 零配置扩展:复制一个目录就能加技能
  2. 适合快速迭代:安全分析师不需要理解中央注册表
  3. 天然支持热加载:目录变化即可触发重扫

它的问题也很明确 #

  1. 安全边界依赖文件系统权限
  2. 技能来源可信度难保证
  3. 缺少签名 / 完整性验证

所以文件系统扫描不是“好设计”或“坏设计”,而是一个很典型的工程取舍:

用部署面简单,换取运行时信任面更脆弱。

如果以后这套系统要走更高安全级别,我会优先加:

  • 技能签名
  • 白名单来源
  • manifest 哈希校验

六、测试 manifest,实际上是在测试框架的拒绝能力 #

manifest 相关测试最重要的不是 happy path,而是 failure path。

def test_skip_on_missing_env_var():
    ...


def test_invalid_cron_format():
    ...


def test_dependency_cycle_rejected():
    ...

这些测试证明的不是“manifest 能被解析”,而是:

  • 系统会不会在坏输入下误注册技能
  • 框架能不能优雅降级
  • 拒绝逻辑是否稳定可复现

这也是为什么我一直强调:测试 manifest,本质上是在测试框架的防线。


七、这一篇真正要记住的框架 #

manifest 的知识框架可以压成一句话:

manifest 不是为了描述技能,而是为了让系统在技能不满足条件时,有充分理由拒绝它。

你可以把它记成四层:

声明能力
  -> 声明依赖
  -> 声明调度
  -> 声明约束

然后由框架把这些声明转成:

允许注册 / 拒绝注册 / 允许执行 / 拒绝执行

这才叫契约。


八、验证命令 #

python main.py list-skills
pytest tests/test_skill_loader.py -v

如果你要专门验证 manifest 失败路径:

pytest tests/ -k "manifest or env_var or cron or dependency" -v