plugin

package
v0.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 26, 2026 License: None detected not legal advice Imports: 0 Imported by: 0

Documentation

Overview

Package plugin 实现插件系统.

插件系统允许通过外部目录扩展 Flyto Agent Engine 的功能. 每个插件是一个包含 plugin.json 清单的目录,可以提供:

  • 技能(markdown 文件,注入到系统提示词)
  • Hook 定义(扩展 hook 系统)
  • MCP 服务器配置

插件搜索路径:

  • ~/.flyto/plugins/ - 用户级插件
  • <project>/.flyto/plugins/ - 项目级插件

插件加载顺序:用户级先加载,项目级后加载,同名时后者覆盖前者.

升华改进(ELEVATED): 相较早期方案只支持 CLI 安装的插件,我们增加了:

  1. RegisterBuiltin - SDK 模式下直接在代码中注册插件,无需文件系统
  2. LoadAll 返回 LoadResult - 区分成功/降级/错误,而非遇到一个错误就中断
  3. Source 字段 - 精确追踪插件来源,支持依赖跨源校验

原方案 <Host.LoadAll 返回 error> - 否决原因:第一个插件加载失败会屏蔽其他所有插件的状态.

Index

Constants

View Source
const ChecksumFileName = "plugin.checksum"

ChecksumFileName 是 plugin 目录下完整性校验 sidecar 文件的约定名称. 文件存在 → plugin 启用完整性校验; 不存在 → SHA256IntegrityVerifier 宽松放行.

View Source
const ChecksumPrefix = "sha256:"

ChecksumPrefix 是当前唯一支持的 checksum 算法前缀. 格式: "sha256:" + 64 hex chars. 未来引入 sha512/blake3 时可扩展 readChecksumFile 同时接受多种前缀.

View Source
const DefaultPluginToolTimeout = 30 * time.Second

DefaultPluginToolTimeout 是 PluginToolDef.TimeoutSeconds 未指定时的默认超时. 30 秒对"快执行的 shell 命令"场景足够, 长 running 的 tool 应显式声明更长值 (或使用 MCP server 路径, 它支持更丰富的生命周期管理).

Variables

View Source
var ErrChecksumMissing = errors.New("plugin.checksum file not found")

ErrChecksumMissing 表示 plugin 目录缺少 plugin.checksum 文件. 通常不会直接暴露给调用方 (SHA256IntegrityVerifier 宽松模式下吞掉这个错误 返回 nil), 只有 RejectUnsignedVerifier 会把它升级成 ErrInvalidSignature.

View Source
var ErrInvalidSignature = errors.New("plugin integrity verification failed")

ErrInvalidSignature 表示 plugin 完整性 / 签名验证失败. 所有 SignatureVerifier 实现失败时都应 wrap 此 sentinel, 便于调用方 errors.Is 判断.

Functions

func ComputeChecksum

func ComputeChecksum(pluginDir string) (string, error)

ComputeChecksum 计算 plugin 目录的完整性哈希 (公开 SDK, 供工具生成器使用).

返回格式: "sha256:<64 hex chars>"

使用场景:

  • 打包工具在发布 plugin 前计算并写入 plugin.checksum
  • CI 管线在 build 后自动生成 checksum
  • `flyto plugin checksum <dir>` CLI 命令 (未来)

本函数和 SHA256IntegrityVerifier.Verify 共用同一份内部算法 computeDirChecksum, 保证生成器和校验器的结果永远一致.

func DefaultSearchPaths

func DefaultSearchPaths(cwd string) []string

DefaultSearchPaths 返回默认的插件搜索路径列表. 搜索路径按优先级从低到高排列(用户级在前,项目级在后).

func FindReverseDependents

func FindReverseDependents(targetName string, all []*Plugin) []string

FindReverseDependents 返回所有直接依赖 targetName 的插件名称列表. 用于"禁用 X 会影响哪些插件"的诊断查询.

func FormatManifestInfo

func FormatManifestInfo(m *Manifest) string

FormatManifestInfo 将清单格式化为人类可读的信息字符串.

func LoadHooks

func LoadHooks(hooksPath string) (map[string][]HookDef, error)

LoadHooks 从 hooks 文件加载 hook 定义.

func NewPluginShellTool

func NewPluginShellTool(def PluginToolDef, pluginDir, pluginName string, executor execenv.Executor) tools.Tool

NewPluginShellTool 从 PluginToolDef 声明构造一个实现 tools.Tool interface 的实例.

公开 factory, 允许消费层在 SDK 嵌入场景里手动构造 tool (而不是走 plugin loader 流程). 参数 pluginName 用于 tool 名命名空间化, 空字符串则不加前缀.

参数:

  • def: PluginToolDef 声明 (来自 plugin.json 或手动构造)
  • pluginDir: plugin 目录, 用于默认 WorkDir 和解析相对路径
  • pluginName: plugin 名, 用于命名空间化 (空则裸 tool 名)
  • executor: 子进程启动抽象 (本地 = execenv.DefaultExecutor{}, 云端 = platform sandbox.Backend). **必填, nil 直接 panic** — 方案 β 严格 DI, 不做静默降级.

默认值填充:

  • InputSchema 空 → emptyObjectSchema ({"type":"object",...})
  • WorkDir 空 → pluginDir
  • WorkDir 相对 → filepath.Join(pluginDir, WorkDir)
  • TimeoutSeconds <=0 → DefaultPluginToolTimeout (30s)
  • PermissionClass 空 → "bash" (shell 命令是天然的 bash class)

func ResolveClosure

func ResolveClosure(names []string, registry map[string]*Plugin) ([]*Plugin, []PluginError)

ResolveClosure 从给定的起点插件集合出发,通过 DFS 计算传递闭包. 返回所有需要加载的插件(包括所有传递依赖),按拓扑顺序排列(依赖先于被依赖者).

参数:

  • names: 用户请求加载的插件名称集合
  • registry: 所有已发现的插件,name → *Plugin

返回:

  • ordered: 拓扑排序后的插件列表(可直接按序加载)
  • errors: 依赖缺失或循环错误(每个受影响插件一条)

算法:三色 DFS

  • 白(未访问):初始状态
  • 灰(访问中):当前 DFS 路径上,发现灰节点 = 有环
  • 黑(已完成):已排入结果

func ValidatePluginConfig

func ValidatePluginConfig(pluginName string, schema []ConfigFieldDef, config map[string]string) error

ValidatePluginConfig 按 schema 验证用户提供的配置 map.

验证规则:

  1. Required=true 的字段必须在 config 中存在(且非空字符串)
  2. 字段的值类型须与 Type 声明匹配(number: 可解析为数字;boolean: "true"/"false")
  3. Secret 字段的值在错误消息中脱敏为 "***"

config 为 nil 等同于空 map(没有任何配置). schema 为空时直接返回 nil(无约束,向后兼容).

精妙之处(CLEVER): 收集所有错误后一次性返回(而非遇第一个错误就停)-- 开发者能一次看到所有缺失/错误的字段,不需要改了一个再发现下一个. 替代方案:第一个错误即返回(快速失败,代码更简单但用户体验差).

func VerifyAndDemote

func VerifyAndDemote(enabled []*Plugin, disabled []*Plugin, loadErrs []PluginError) (
	newEnabled []*Plugin,
	newDisabled []*Plugin,
	warnings []PluginError,
)

VerifyAndDemote 固定点迭代:如果某个插件的依赖被禁用或有错误, 将该插件从 enabled 移入 disabled,并记录原因.

算法:固定点(fixed-point)迭代 - 反复扫描直到没有新的降级发生.

精妙之处(CLEVER): 为什么用固定点迭代而不是拓扑排序倒序遍历? 因为输入 enabled 列表不保证已经拓扑排序.固定点迭代不依赖顺序, 正确处理任意顺序的输入列表.最坏情况 O(n²),实际插件数量小(<100),可接受. 如果改用拓扑排序倒序,需要先建图,实现更复杂,收益微小.

func WriteChecksumFile

func WriteChecksumFile(pluginDir string) (string, error)

WriteChecksumFile 计算 plugin 目录哈希并写入 pluginDir/plugin.checksum.

幂等: 已存在的 checksum 文件被覆盖, 且 computeDirChecksum 会自动排除 plugin.checksum 自身, 所以多次调用都会产生相同内容 (只要目录内容没变).

返回写入的完整 checksum 字符串 (含 "sha256:" 前缀), 供调用方打 log 或回显.

Types

type BuiltinDef

type BuiltinDef struct {
	// Name 内置插件名称(必须唯一,不能与已注册插件冲突)
	Name string
	// Description 描述
	Description string
	// Version 版本,可留空(默认 "0.0.0")
	Version string
	// Skills 内置技能列表
	Skills []*Skill
	// Hooks 内置 hook 定义
	Hooks map[string][]HookDef
	// MCPServers 内置 MCP 服务器配置
	MCPServers []MCPServerDef
}

BuiltinDef 是内置插件的注册信息.

升华改进(ELEVATED): SDK 嵌入场景下,消费层可以将内置能力(如仓储查询工具) 打包为内置插件注册,享受与文件系统插件相同的 enable/disable/skill 聚合接口, 无需在代码和插件系统之间维护两套资源管理逻辑. 原方案 <直接向 Host 注入 Skills 列表> - 否决原因:绕过了插件生命周期管理(enable/disable), 且与文件系统插件的统一查询接口不兼容.

type ConfigFieldDef

type ConfigFieldDef struct {
	// Key 配置项的键名(在 user config map 中对应的 key)
	Key string `json:"key"`
	// Type 值类型:"string" / "number" / "boolean"
	// 空字符串时默认视为 "string"
	Type string `json:"type"`
	// Required 是否为必填项.
	// true 时若用户配置 map 中不存在该 key,ValidatePluginConfig 返回错误.
	Required bool `json:"required"`
	// Default 默认值(字符串表示,非必填项可以有默认).
	// 校验时若 key 缺失且 Required==false,可用此值填充.
	// 历史包袱(LEGACY): 当前校验器不自动填充默认值(只报告缺失),
	// 调用方(Host.SetPluginConfig)负责应用默认值.
	// 未来如有需要,可在 ValidatePluginConfig 中补充自动填充逻辑.
	Default string `json:"default,omitempty"`
	// Description 人类可读的字段描述(用于 help 输出和 UI 展示)
	Description string `json:"description,omitempty"`
	// Secret 标记此字段是否为敏感值(如 API Key,密码).
	// true 时日志/错误消息中对该字段值进行脱敏(显示为 "***").
	//
	// 升华改进(ELEVATED): Secret 标记让引擎主动保护敏感值--
	// 插件作者只需声明"这是秘密",引擎层统一处理脱敏,
	// 无需每个插件自己实现 mask 逻辑.
	// 替代方案:<命名约定(key 包含 "secret"/"password" 的自动脱敏)>
	// - 否决:名称约定脆弱,容易遗漏("api_key" vs "apiKey" vs "key").
	Secret bool `json:"secret"`
}

ConfigFieldDef 是插件配置字段的定义(12.6 Plugin Config Schema).

字段 Type 使用 JSON Schema 风格的基础类型:

"string"  - 字符串(最常见:API Key,endpoint URL)
"number"  - 数字(整数或浮点,如超时秒数,并发限制)
"boolean" - 布尔(true/false,如是否开启调试模式)

精妙之处(CLEVER): 只支持 3 种基础类型而非完整 JSON Schema-- 插件配置通常只需要 string/number/boolean,完整的 $ref/$defs/oneOf 反而 增加了实现复杂度和消费层解析成本.若未来需要复杂类型(array/object), 可在 Type 中增加,向后兼容(旧校验器遇到未知类型时 fallback 为 string).

type Definition

type Definition struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	Source      string `json:"source"` // 插件标识符
}

Definition 是插件定义(旧接口兼容).

type HookDef

type HookDef struct {
	// Command 要执行的 shell 命令
	Command string `json:"command"`
	// Timeout 超时时间(秒),默认 30
	Timeout int `json:"timeout"`
}

HookDef 是单个 hook 的定义.

type HooksDef

type HooksDef struct {
	// Hooks 按 hook 类型分组的 hook 定义列表
	Hooks map[string][]HookDef `json:"hooks"`
}

HooksDef 是 hooks 配置文件的结构.

type Host

type Host struct {
	// contains filtered or unexported fields
}

Host 是插件宿主,管理所有插件的加载,启用/禁用和资源访问. 是消费层与插件系统交互的唯一入口.

线程安全:所有操作通过 sync.RWMutex 保护.

func NewHost

func NewHost(executor execenv.Executor) *Host

NewHost 创建一个新的插件宿主.

executor 必填, nil panic. 方案 β 严格 DI 要求所有子进程启动点都走 统一 execenv.Executor 抽象, 禁止静默降级. 本地 CLI 传 execenv.DefaultExecutor{}, 云端 platform 层传 sandbox backend.

func (*Host) Close

func (h *Host) Close()

Close 关闭所有插件,释放资源.

func (*Host) Count

func (h *Host) Count() int

Count 返回已加载的插件数量.

func (*Host) Disable

func (h *Host) Disable(name string) error

Disable 禁用指定名称的插件.

func (*Host) Enable

func (h *Host) Enable(name string) error

Enable 启用指定名称的插件.

func (*Host) Get

func (h *Host) Get(name string) (*Plugin, bool)

Get 获取指定名称的插件.

func (*Host) GetAllHooks

func (h *Host) GetAllHooks() map[string][]HookDef

GetAllHooks 收集所有启用插件提供的 hook 定义.

func (*Host) GetAllMCPServers

func (h *Host) GetAllMCPServers() []MCPServerDef

GetAllMCPServers 收集所有启用插件提供的 MCP 服务器配置.

func (*Host) GetAllSkills

func (h *Host) GetAllSkills() []*Skill

GetAllSkills 收集所有启用插件提供的技能.

func (*Host) GetAllTools

func (h *Host) GetAllTools() []tools.Tool

GetAllTools 收集所有启用插件通过 plugin.json 的 tools 字段声明式注册的 tool 实例.

与 GetAllMCPServers 的区别: GetAllMCPServers 返回的是 **配置** (MCPServerDef), 调用方还要自己启动 subprocess 并走 MCP 协议发现 tool. GetAllTools 返回的是 **已实例化**的 tools.Tool, 调用方可以直接 `tools.Registry.Register(t)`.

使用场景: 消费层 (Host/engine 启动时) 调用此方法收集所有 declarative tool, 注册到全局 Registry, 让 agent 可以直接调用. 典型代码:

for _, t := range host.GetAllTools() {
    if err := registry.Register(t); err != nil {
        log.Printf("register tool %q: %v", t.Name(), err)
    }
}

精妙之处 (CLEVER): 本方法返回类型是 []tools.Tool 而非 []*pluginShellTool, 因为未来 plugin 可能通过别的机制贡献 tool (WASM / Go static binary / 内置 built-in tool 提供器), 接口类型能无缝包含所有实现.

func (*Host) GetPluginConfig

func (h *Host) GetPluginConfig(name string) map[string]string

GetPluginConfig 获取指定插件的完整配置 map.

返回值是配置的副本(修改不影响内部存储). 若未设置配置,返回空 map(非 nil).

升华改进(ELEVATED): 插件通过此方法读取自己的配置,隔离命名空间-- 不同插件的 "api_key" 字段各自独立,无需前缀拼接. 替代方案:<直接暴露 pluginConfigStore> - 否决:绕过了 Host 的封装,外部可能破坏内部一致性.

func (*Host) GetPluginConfigField

func (h *Host) GetPluginConfigField(name, key string) (string, bool)

GetPluginConfigField 获取指定插件的单个配置字段值. 若字段不存在,返回空字符串和 false.

func (*Host) List

func (h *Host) List() []*Plugin

List 列出所有已加载的插件.

func (*Host) ListEnabled

func (h *Host) ListEnabled() []*Plugin

ListEnabled 列出所有启用的插件.

func (*Host) Load

func (h *Host) Load(def Definition) error

Load registers a single plugin definition as a metadata-only placeholder in the host registry. It does NOT parse skills / hooks / tools / MCPServers -- those come from LoadFromDir (which reads plugin.json on disk) or RegisterBuiltin (in-binary). Use this when the caller wants the host to know a plugin exists but has no plugin.json at hand, e.g. SDK tests or engine.Config.Plugins wiring.

The created Plugin is marked Enabled=true and pinned to SourceUser so RemoveByScope(SourceUser) sweeps it alongside real disk-loaded user plugins. The Source enum value is a coarse tag; finer "came from Config.Plugins" attribution is the caller's responsibility if needed.

Load 把一个 plugin 定义作为 metadata-only 占位登记到 host registry. 不解析 skills / hooks / tools / MCPServers -- 那些走 LoadFromDir (读磁盘上的 plugin.json) 或 RegisterBuiltin (内嵌二进制). 调用方想 让 host 知道某 plugin 存在但手头没 plugin.json 时用这条, e.g. SDK 测试或 engine.Config.Plugins wiring.

创建的 Plugin 标记 Enabled=true, Source 钉为 SourceUser, 这样 RemoveByScope(SourceUser) 扫的时候能跟真正从磁盘加载的 user plugin 一起清. Source 枚举是粗分类 tag; 如果需要更细的 "来自 Config.Plugins" 归因, 由调用方自己记.

func (*Host) LoadAll

func (h *Host) LoadAll(searchPaths []string) LoadResult

LoadAll 发现并加载所有搜索路径下的插件.

加载流程:

  1. 按路径顺序发现所有插件目录(后发现的同名插件覆盖先发现的)
  2. 完整加载每个插件(清单→技能→hooks→MCP)
  3. 依赖解析:ResolveClosure 计算传递闭包
  4. VerifyAndDemote:依赖缺失/错误的插件降级禁用
  5. 更新 Host 内部状态

返回 LoadResult,调用方可以按需处理 Errors/Warnings, 不会因为单个插件问题中断其他插件的加载.

func (*Host) LoadFromDir

func (h *Host) LoadFromDir(dir string) error

LoadFromDir 从指定目录加载并注册一个插件.

func (*Host) RegisterBuiltin

func (h *Host) RegisterBuiltin(def BuiltinDef) error

RegisterBuiltin 在内存中注册一个内置插件. 内置插件不来自文件系统,由 SDK 消费者在代码中直接注册.

若同名内置插件已存在则覆盖(允许重复注册,方便测试). 若与已加载的文件系统插件同名,返回 ErrBuiltinConflict 错误.

func (*Host) SetPluginConfig

func (h *Host) SetPluginConfig(name string, config map[string]string) error

SetPluginConfig 设置指定插件的配置 map,并按 schema 验证配置有效性.

调用时机:消费层在 LoadAll/LoadFromDir 之后,Run 之前调用-- 提供插件运行所需的配置(API Key,endpoint 等).

若插件未加载(name 不在注册表中),仍接受配置设置(允许预配置后再加载). 若插件有 ConfigSchema,ValidatePluginConfig 验证 Required 字段和类型约束; 验证失败返回 PluginError{Code: ErrConfigValidation}.

精妙之处(CLEVER): 先验证再存储--无效配置不会写入存储, 确保 GetPluginConfig 返回的配置一定通过了 schema 校验. 替代方案:先存储后验证(消费层可能用到未验证的配置,带来不一致风险).

type LoadResult

type LoadResult struct {
	// Enabled 成功加载并启用的插件列表
	Enabled []*Plugin
	// Disabled 因依赖降级而被禁用的插件列表
	// 精妙之处(CLEVER): 保留禁用插件而非直接丢弃,使消费层可以展示"因 X 禁用了 Y"的诊断信息.
	Disabled []*Plugin
	// Errors 致命错误列表(插件完全无法加载)
	Errors []PluginError
	// Warnings 非致命警告列表(插件加载但有降级)
	Warnings []PluginError
}

LoadResult 是 LoadAll 操作的完整结果.

升华改进(ELEVATED): 早期方案直接返回 error,第一个失败就停止. 我们区分 Enabled/Disabled/Errors/Warnings,使单个插件的问题不阻断其他插件加载. CLI 消费者可以在启动时打印 Warnings 而不中断;SDK 消费者可以编程式处理 Errors. 原方案 <返回 []error> - 否决原因:丢失了"哪些加载成功了"的信息,消费层无法区分部分成功.

func (*LoadResult) HasErrors

func (r *LoadResult) HasErrors() bool

HasErrors 返回是否有致命错误.

func (*LoadResult) Summary

func (r *LoadResult) Summary() string

Summary 返回加载结果的人类可读摘要. 适合在启动日志或 CLI 输出中展示.

type MCPDef

type MCPDef struct {
	// Transport 传输方式:stdio / sse / http / ws
	Transport string `json:"transport"`
	// Command stdio 模式下的可执行命令
	Command string `json:"command,omitempty"`
	// Args 命令参数
	Args []string `json:"args,omitempty"`
	// URL sse/http/ws 模式下的服务器 URL
	URL string `json:"url,omitempty"`
	// Env 环境变量
	Env map[string]string `json:"env,omitempty"`
}

MCPDef 是 MCP 服务器定义.

type MCPServerDef

type MCPServerDef struct {
	// Name 服务器名称
	Name string
	// PluginName 来源插件名称
	PluginName string
	// MCPDef 服务器配置
	MCPDef
}

MCPServerDef 是带有来源信息的 MCP 服务器定义.

type Manifest

type Manifest struct {
	// Name 插件名称(唯一标识符)
	Name string `json:"name"`
	// Description 插件描述
	Description string `json:"description"`
	// Version 插件版本(语义化版本号)
	Version string `json:"version"`
	// SkillsPath 技能文件目录的相对路径(相对于插件目录)
	SkillsPath string `json:"skills_path"`
	// HooksPath hooks 配置文件的相对路径
	HooksPath string `json:"hooks_path"`
	// MCPServers MCP 服务器配置.
	//
	// 升华改进(ELEVATED): JSON 字段名从 "mcp_servers" 改为 "mcpServers",
	// 与 Claude Code 插件格式对齐.这样同一个 plugin.json 可直接被 Claude Code 读取,
	// 无需手动转换,降低跨平台插件的维护成本.
	// 早期方案字段名 <"mcp_servers"> - 保留为向后兼容在下方 UnmarshalJSON 中处理.
	MCPServers map[string]MCPDef `json:"mcpServers"`
	// Dependencies 依赖的其他插件名称列表
	Dependencies []string `json:"dependencies"`
	// ConfigSchema 插件配置字段定义(12.6 Plugin Config Schema).
	//
	// 升华改进(ELEVATED): 插件声明自己需要哪些配置项,引擎在加载时验证用户提供的
	// 配置是否满足 Required 字段,并提供类型约束和描述.
	// 等价于 npm package.json 里的 peerDependencies + engines 约束--
	// 消费层不必逐一阅读插件源码就能知道"这个插件需要我提供什么".
	// 替代方案:<文档约定(README 里写需要什么环境变量)>
	// - 否决:文档和代码分离,容易过期;机器无法校验,只能靠人工排查.
	ConfigSchema []ConfigFieldDef `json:"config_schema,omitempty"`

	// Tools 是 plugin 声明式注册的 shell tool 列表 (2026-04-15 新增).
	//
	// 每个 PluginToolDef 在加载时被 loadPluginTools 翻译成一个 pluginShellTool
	// 实例 (实现 tools.Tool interface), 存放在 Plugin.Tools 里, 供消费层
	// (Host.GetAllTools) 注册到全局 tools.Registry.
	//
	// 这是 plugin 扩展 tool 的**第二条路径** (第一条是 MCPServers 配置).
	// 适用场景: 单 tool / 无状态 / shell 脚本就能搞定的简单扩展.
	// 详见 plugin_tool.go 的 "为什么同时需要 MCP server 和 declarative tool" 段落.
	Tools []PluginToolDef `json:"tools,omitempty"`
}

Manifest 是插件清单的结构体表示. 对应插件目录下的 plugin.json 文件.

func LoadManifest

func LoadManifest(pluginDir string) (*Manifest, error)

LoadManifest 从插件目录加载并解析清单文件.

参数:

  • pluginDir: 插件目录的绝对路径

返回解析后的清单和错误. 如果 plugin.json 不存在或格式错误,返回相应的错误.

func (*Manifest) UnmarshalJSON

func (m *Manifest) UnmarshalJSON(data []byte) error

UnmarshalJSON 自定义反序列化,同时支持新字段名(mcpServers)和旧字段名(mcp_servers).

精妙之处(CLEVER): 用 type alias 的嵌套 struct 技巧,避免 UnmarshalJSON 无限递归: manifestAlias 与 Manifest 有相同的字段(通过嵌入),但没有 UnmarshalJSON 方法, 所以 json.Unmarshal 会走默认路径而不会再次调用此函数. legacyFields 只声明旧字段名,两者都解码后再合并--新字段优先,旧字段兜底.

type NoopVerifier

type NoopVerifier struct{}

NoopVerifier 是"零校验"实现, 对任何输入都返回 nil.

用途:

  • DefaultVerifier() 默认返回此实现 (保持向后兼容)
  • 单元测试中不关心完整性的场景
  • 本地开发时临时禁用校验

注意: 生产部署中如果启用了完整性校验, 应该用 SHA256IntegrityVerifier (宽松) 或 RejectUnsignedVerifier (严格), 不要直接用 NoopVerifier.

精妙之处 (CLEVER): NoopVerifier 是空 struct, 零内存占用, 天然线程安全. 即便 LoadAll 并发加载 100 个 plugin, 共用一个实例也毫无代价.

func (NoopVerifier) Verify

func (NoopVerifier) Verify(pluginDir string, manifest *Manifest) error

Verify 对 NoopVerifier 永远返回 nil.

type Plugin

type Plugin struct {
	// Name 插件名称(唯一标识符)
	Name string
	// Description 插件描述
	Description string
	// Version 插件版本
	Version string
	// Path 插件目录的绝对路径(内置插件为空字符串)
	Path string
	// Source 插件的来源级别(内置/用户级/项目级)
	// 影响依赖解析中的跨来源规则.
	Source Source
	// Manifest 插件清单(plugin.json 的解析结果);内置插件为 nil
	Manifest *Manifest
	// Enabled 插件是否启用
	Enabled bool
	// Skills 插件提供的技能列表
	Skills []*Skill
	// Hooks 插件提供的 hook 定义,key 是 hook 类型
	Hooks map[string][]HookDef
	// MCPServers 插件提供的 MCP 服务器配置
	MCPServers []MCPServerDef
	// Tools 插件通过 plugin.json 的 tools 字段声明式注册的 shell tool 运行时实例.
	//
	// 区别于 MCPServers 字段: MCPServers 是配置 (启动外部 MCP server 进程),
	// Tools 是已经实例化的 tools.Tool, 可以直接注册到 tools.Registry.
	// 详见 plugin_tool.go 的 "为什么同时需要 MCP server 和 declarative tool
	// 两条路径" 段落.
	//
	// 消费层 (Host 或 engine) 通过 Host.GetAllTools() 收集所有启用插件的 tools,
	// 并调用 tools.Registry.Register() 逐个注册.
	Tools []tools.Tool
}

Plugin 是一个已加载的插件实例.

func DiscoverPlugins

func DiscoverPlugins(searchPaths []string, executor execenv.Executor) []*Plugin

DiscoverPlugins 发现所有可用的插件(旧接口兼容). 返回发现的插件列表(尚未完全加载,仅解析了清单). executor 透传给每个 LoadPlugin 调用, 本地模式传 execenv.DefaultExecutor{}.

func LoadPlugin

func LoadPlugin(dir string, executor execenv.Executor) (*Plugin, error)

LoadPlugin 完整加载一个插件 (公开接口, 向后兼容). 参数 dir 是插件目录路径 (必须包含 plugin.json). 错误时返回第一个致命错误 (如有).

本函数使用 DefaultVerifier (当前是 NoopVerifier, 不做完整性校验). 消费层如需启用完整性校验, 使用 LoadPluginWithVerifier 并传入:

  • NewSHA256IntegrityVerifier() 宽松模式 (有 plugin.checksum 才校验)
  • RejectUnsignedVerifier{Inner: ...} 严格模式 (必须有 plugin.checksum)

executor 必填 (方案 β 严格 DI), 透传给每个 pluginShellTool. 详见 integrity.go 和 signature.go.

func LoadPluginWithVerifier

func LoadPluginWithVerifier(dir string, verifier SignatureVerifier, executor execenv.Executor) (*Plugin, error)

LoadPluginWithVerifier 加载插件并执行完整性 / 签名验证.

参数:

  • dir: 插件目录路径 (必须包含 plugin.json)
  • verifier: 完整性验证器 (nil 则用 DefaultVerifier, 当前是 NoopVerifier)

执行顺序:

  1. 加载 manifest
  2. 调用 verifier.Verify(dir, manifest) - 策略由 verifier 自主决定 (宽松 / 严格 / 零校验)
  3. 验证通过才继续走 loadPluginWithErrors 加载 skills/hooks/MCP
  4. 验证失败返回 wrap 后的 error (通常 errors.Is(err, ErrInvalidSignature))

宽松 vs 严格策略**不在 loader 层做判断**, 完全交给 verifier:

  • NoopVerifier: 零校验
  • SHA256IntegrityVerifier: plugin.checksum 不存在放行, 存在则校验
  • RejectUnsignedVerifier: plugin.checksum 不存在直接拒绝

这种 "loader 调度 + verifier 策略" 的分离让未来新增 verifier 实现 (Ed25519 / Sshsig / Sigstore / PKCS#7) 不需要改 loader 一行代码.

type PluginError

type PluginError struct {
	// Code 错误分类码
	Code PluginErrorCode
	// PluginName 触发错误的插件名称
	PluginName string
	// Message 人类可读的错误描述
	Message string
	// Cause 底层原因(可选)
	Cause error
}

PluginError 是一个结构化的插件错误,包含错误码,上下文和原始原因. 实现 error 接口,可直接用于 errors.Is/As 链.

func (PluginError) Error

func (e PluginError) Error() string

Error 实现 error 接口.

func (PluginError) Unwrap

func (e PluginError) Unwrap() error

Unwrap 实现 errors.Unwrap,支持 errors.Is/As 链式检查.

type PluginErrorCode

type PluginErrorCode int

PluginErrorCode 是插件错误码枚举. 10 种类型覆盖从清单到依赖的全路径错误场景.

精妙之处(CLEVER): 用 iota+1 而非 iota,使零值(0)明确表示"无错误", 避免 PluginErrorCode 的零值被误判为有效错误码.

const (
	// ErrManifestNotFound plugin.json 文件不存在
	ErrManifestNotFound PluginErrorCode = iota + 1
	// ErrManifestInvalid plugin.json 格式非法(JSON 语法错误)
	ErrManifestInvalid
	// ErrManifestValidation 清单内容验证失败(必填字段缺失,非法字符等)
	ErrManifestValidation
	// ErrSkillsLoadFailed 技能目录加载失败(读权限,文件损坏等)
	ErrSkillsLoadFailed
	// ErrHooksLoadFailed hooks 配置文件加载失败
	ErrHooksLoadFailed
	// ErrMCPConfigInvalid MCP 服务器配置无效(缺少必填字段)
	ErrMCPConfigInvalid
	// ErrDependencyMissing 依赖的插件未找到(未安装或未在搜索路径)
	ErrDependencyMissing
	// ErrDependencyCycle 依赖关系中存在循环(A→B→A)
	ErrDependencyCycle
	// ErrDependencyCrossSource 跨来源依赖不允许(项目级插件依赖用户级插件)
	// 历史包袱(LEGACY): 当前以插件名前缀推断来源,未来应改为插件注册时携带
	// 显式的 Source 字段,以实现精确的来源隔离.
	ErrDependencyCrossSource
	// ErrBuiltinConflict 与内置插件名称冲突
	ErrBuiltinConflict
	// ErrConfigValidation 插件配置验证失败(Required 字段缺失 / 类型不匹配).
	// 12.6 Plugin Config Schema - 用户提供的配置不满足 plugin.json 中 config_schema 的约束.
	ErrConfigValidation
)

type PluginToolDef

type PluginToolDef struct {
	// Name 是 tool 的本地名称 (不含 plugin 前缀).
	// 最终注册到 Registry 的名字是 "pluginName:Name".
	// 必填, 空值会被 loadPluginTools 跳过.
	Name string `json:"name"`

	// Description 是给 LLM 的 tool 描述. 必填 (否则 LLM 不知道该不该调).
	Description string `json:"description"`

	// InputSchema 是 tool 参数的 JSON Schema. 可选, 缺省为空对象 schema
	// ({"type":"object","properties":{},"additionalProperties":true}),
	// 即"接受任意 JSON 对象"- 适合无参数或参数非常灵活的 tool.
	InputSchema json.RawMessage `json:"input_schema,omitempty"`

	// Command 是要执行的命令 (可以是 $PATH 里的名字如 "python", 或绝对/
	// 相对路径如 "/usr/bin/env" / "./script.sh"). 必填.
	// 相对路径会相对于 WorkDir (默认为 pluginDir) 解析, 由 exec.Command 处理.
	Command string `json:"command"`

	// Args 是传给 Command 的命令行参数 (不含 command 本身).
	Args []string `json:"args,omitempty"`

	// Env 是要注入子进程环境变量的 key/value. 会叠加到白名单 OS env
	// (PATH/HOME/LANG/LC_ALL) 之上, 同名变量以 Env 里的为准. Value 支持
	// ${HOST_VAR} / $HOST_VAR 展开宿主 env, 未设置的引用会导致 tool error.
	// 常用于传 API Key / endpoint / plugin config. 详见 pkg/execenv 包.
	Env map[string]string `json:"env,omitempty"`

	// WorkDir 是子进程的工作目录. 可选:
	//   - 空字符串 (默认): 使用 pluginDir
	//   - 相对路径: 相对于 pluginDir 解析
	//   - 绝对路径: 直接使用
	WorkDir string `json:"work_dir,omitempty"`

	// TimeoutSeconds 是子进程执行的超时时间 (秒). <=0 或未指定 → 30s 默认.
	// 超时后进程被 SIGKILL, Execute 返回 IsError=true 的 Result.
	TimeoutSeconds int `json:"timeout_seconds,omitempty"`

	// PermissionClass 是权限检查类型 (参见 tools.Metadata.PermissionClass).
	// 空字符串默认 "bash" (命令执行类, 子命令分割 + 危险命令检测).
	PermissionClass string `json:"permission_class,omitempty"`

	// ConcurrencySafe 声明本 tool 是否可以与其他 tool 并发执行.
	// 保守默认 false (和 tools.GetMetadata 的 fallback 一致).
	ConcurrencySafe bool `json:"concurrency_safe,omitempty"`

	// ReadOnly 声明本 tool 是否只读 (无副作用).
	// 保守默认 false.
	ReadOnly bool `json:"read_only,omitempty"`

	// Destructive 声明本 tool 是否会造成不可逆的破坏
	// (rm / DROP TABLE / kubectl delete 等).
	// 保守默认 false.
	Destructive bool `json:"destructive,omitempty"`
}

PluginToolDef 是 plugin.json 的 tools 字段里的一个 tool 声明.

每个声明在 plugin 加载时被 loadPluginTools 翻译成一个 pluginShellTool 并加入 Plugin.Tools 列表, 供消费层 (Host 或 engine 层) 注册到全局 tools.Registry.

JSON 示例:

{
  "name": "git_branch",
  "description": "Return the current git branch name",
  "command": "git",
  "args": ["branch", "--show-current"],
  "timeout_seconds": 5,
  "read_only": true,
  "permission_class": "readonly"
}

type RejectUnsignedVerifier

type RejectUnsignedVerifier struct {
	// Inner 是实际执行验证的 verifier. 通常是 NewSHA256IntegrityVerifier().
	// 为 nil 时默认使用 SHA256IntegrityVerifier{}.
	Inner SignatureVerifier
}

RejectUnsignedVerifier 是严格模式装饰器: 拒绝加载没有 plugin.checksum 的 目录, 有 checksum 的则 delegate 给 Inner 做实际验证.

使用示例 (企业合规场景):

strict := plugin.RejectUnsignedVerifier{
    Inner: plugin.NewSHA256IntegrityVerifier(),
}
p, err := plugin.LoadPluginWithVerifier(dir, strict)

Inner 为 nil 时默认使用 SHA256IntegrityVerifier{} 作为底层验证器.

精妙之处 (CLEVER): 用 decorator 模式而不是在 SHA256IntegrityVerifier 里 开一个 StrictMode bool 字段, 因为策略组合能力更强 - 未来可以用 RejectUnsignedVerifier 包装 Ed25519Verifier / Pkcs7Verifier 等任意实现, 而不用每个 verifier 都重复实现严格/宽松切换.

func (RejectUnsignedVerifier) Verify

func (r RejectUnsignedVerifier) Verify(pluginDir string, manifest *Manifest) error

Verify 实现 SignatureVerifier 接口.

type SHA256IntegrityVerifier

type SHA256IntegrityVerifier struct{}

SHA256IntegrityVerifier 是基于 plugin.checksum sidecar 文件的完整性验证器.

行为 (宽松模式):

  • plugin.checksum 不存在: 返回 nil (不校验, 向后兼容已有 plugin)
  • plugin.checksum 存在: 计算目录哈希并比对, 不一致返回 ErrInvalidSignature

严格模式通过 RejectUnsignedVerifier 包装获得 (拒绝无 checksum 的 plugin).

精妙之处 (CLEVER): 这是一个空 struct, 无状态, 天然线程安全. LoadAll 并发加载多个 plugin 时共用一个 verifier 实例无竞态.

func (SHA256IntegrityVerifier) Verify

func (SHA256IntegrityVerifier) Verify(pluginDir string, manifest *Manifest) error

Verify 实现 SignatureVerifier 接口.

manifest 参数当前未使用 (plugin.checksum 是独立 sidecar, 与 manifest 解耦), 保留是为了未来扩展: Ed25519Verifier 可能需要读 manifest.Name 做错误上下文 或根据 manifest 字段做策略差异.

type SignatureVerifier

type SignatureVerifier interface {
	// Verify 验证 plugin 目录的完整性 / 签名.
	//
	// 参数:
	//   - pluginDir: plugin 目录的绝对路径
	//   - manifest:  已加载的 Manifest (可能为 nil, 虽然 LoadPluginWithVerifier
	//                总是传非 nil, 但 verifier 应该容错处理)
	//
	// 返回 nil 表示通过, 非 nil 表示拒绝加载.
	Verify(pluginDir string, manifest *Manifest) error
}

SignatureVerifier 是 plugin 完整性 / 签名验证器接口.

调用时机: LoadPluginWithVerifier 在加载 manifest 后, 加载 skills/hooks/MCP 前 调用 Verify. 返回非 nil 即拒绝加载整个 plugin.

实现必须**无状态 + 线程安全**, 因为同一个 Verifier 会被 LoadAll 并发调用多次.

**宽松 vs 严格策略不在 interface 层预设**, 由实现自己决定:

  • NoopVerifier: 永远放行 (零校验)
  • SHA256IntegrityVerifier: plugin.checksum 不存在放行, 存在则校验
  • RejectUnsignedVerifier: 包装 Inner, 无 checksum 时直接拒绝 (decorator 模式)

调用方通过**组合 verifier** 来表达策略, 而不是在 interface 里开 bool 参数. 这种 "verifier 自治策略" 的设计让 loader 保持简单 (只管调 Verify), 同时支持未来任意策略组合 (e.g. "宽松 + 白名单" / "严格 + Sigstore fallback").

Shape: synchronous callback. Loader calls Verify synchronously before activating a plugin; verifier consults checksum / signature / trust store as needed and returns accept or rejection reason.

形态: 同步回调. Loader 在激活 plugin 前同步调 Verify; verifier 按需查 checksum / 签名 / 信任存储, 返回接受或拒绝原因.

func DefaultVerifier

func DefaultVerifier() SignatureVerifier

DefaultVerifier 返回默认的 SignatureVerifier.

当前默认是 NoopVerifier (向后兼容, 不强制完整性校验). 消费层若想启用完整性校验, 应显式传入 NewSHA256IntegrityVerifier() 到 LoadPluginWithVerifier.

未来可能切换默认为 SHA256IntegrityVerifier (宽松模式): 当生态成熟到大部分 plugin 都会 ship plugin.checksum 文件时, 默认启用宽松模式完整性校验是 无摩擦的 (没 checksum 的 plugin 继续放行, 有的被校验). 那个时点的切换由 产品决策, 不是代码决策, 改这里一行即可.

替代方案: <硬编码 NoopVerifier 到 LoadPlugin 内部> - 否决, 独立函数让 未来切换默认值只需改一行, 不需要在 loader.go 里做 search-replace.

func NewSHA256IntegrityVerifier

func NewSHA256IntegrityVerifier() SignatureVerifier

NewSHA256IntegrityVerifier 创建一个 SHA-256 完整性验证器.

使用示例:

v := plugin.NewSHA256IntegrityVerifier()
p, err := plugin.LoadPluginWithVerifier(dir, v)

type Skill

type Skill struct {
	// Name 技能名称(命名空间化后的名称,格式:pluginName:skillName)
	Name string
	// RawName 技能原始名称(不含插件前缀)
	RawName string
	// PluginName 来源插件名称
	PluginName string
	// Description 技能描述
	Description string
	// WhenToUse 何时使用此技能的说明
	WhenToUse string
	// AllowedTools 此技能允许使用的工具列表
	AllowedTools []string
	// Content 技能正文内容(markdown)
	Content string
	// FilePath 技能文件的原始路径
	FilePath string
}

Skill 是一个插件技能定义. 结构与 engine.Skill 兼容,但增加了插件来源信息.

func LoadPluginSkills

func LoadPluginSkills(skillsDir, pluginName string) ([]*Skill, error)

LoadPluginSkills 从插件的技能目录加载所有技能文件.

参数:

  • skillsDir: 技能目录的绝对路径
  • pluginName: 插件名称(用于命名空间化)

返回加载的技能列表.跳过无法解析的文件.

func (*Skill) FormatAsPrompt

func (s *Skill) FormatAsPrompt() string

FormatAsPrompt 将技能格式化为系统提示词片段.

type Source

type Source int

Source 表示插件的来源级别. 来源决定了依赖规则:只允许同级或向上(项目级→用户级不允许).

精妙之处(CLEVER): 用整数而非字符串,使来源比较可以用 >= 运算符. SourceBuiltin=0 < SourceUser=1 < SourceProject=2,越大优先级越高. 如果改用字符串比较,跨来源逻辑会更复杂.

const (
	// SourceBuiltin 内置插件(代码注册,无文件系统来源)
	SourceBuiltin Source = iota
	// SourceUser 用户级插件(~/.flyto/plugins/)
	SourceUser
	// SourceProject 项目级插件(<cwd>/.flyto/plugins/)
	SourceProject
)

type ValidationResult

type ValidationResult struct {
	// Valid 清单是否通过所有验证
	Valid bool
	// Errors 阻断加载的验证错误
	Errors []PluginError
	// Warnings 不阻断加载的警告(如建议字段缺失)
	Warnings []PluginError
}

ValidationResult 是 ValidateManifest 操作的结果. 与 LoadResult 分离,支持在实际加载前做"预检".

func ValidateManifest

func ValidateManifest(dir string) ValidationResult

ValidateManifest 主动验证插件目录的清单,不实际加载插件. 用于安装/更新时的预检,或 lint 工具.

与 LoadManifest 的区别:

  • ValidateManifest 返回结构化 ValidationResult(含 Warnings)
  • LoadManifest 只返回 error(pass/fail)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL