package evolve // ToolBuilder 让 Agent 在运行时定义新工具. // // 这是 C 方案最核心的能力:Agent 发现当前工具集无法解决某个问题时, // 可以自己编写一个新工具(本质是一段 shell 脚本或 Go 插件), // 注册到 Engine 中,然后立即使用. // // 安全模型: // - 新工具的代码必须经过人类审批(通过 ApprovalFunc) // - 新工具默认 ConcurrencySafe=false, ReadOnly=false(最保守) // - 新工具的执行受 Engine 的权限系统管控(和内置工具一样) // - 每个会话有工具创建数量上限(防止失控) // // 工具持久化: // - 新工具保存为 JSON 定义 + shell 脚本 // - 下次启动 Engine 时可以自动加载之前创建的工具 // - 每个工具有版本号,支持迭代改进 import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "git.flytoex.net/yuanwei/flyto-agent/pkg/execenv" "git.flytoex.net/yuanwei/flyto-agent/pkg/security" ) // ToolBuilder 负责运行时工具构建. type ToolBuilder struct { store *EvolutionStore maxPerSession int created int // 本会话已创建数 guard security.SecretGuard // 秘密扫描(可选) } // NewToolBuilder 创建工具构建器. func NewToolBuilder(store *EvolutionStore, maxPerSession int) *ToolBuilder { return &ToolBuilder{ store: store, maxPerSession: maxPerSession, } } // NewToolBuilderWithGuard 创建带秘密扫描的工具构建器. // // 升华改进(ELEVATED): tool_builder 写入的 Script 字段可能含有 API key-- // Agent 在生成脚本时可能无意间将当前 session 的环境变量硬编码进脚本. // SecretGuard 在持久化前拦截,阻止 key 被写入磁盘(高风险路径). // 替代方案:<不扫描,依靠 Agent 不犯错> - 否决原因:安全不能依赖 Agent 的"正确性". func NewToolBuilderWithGuard(store *EvolutionStore, maxPerSession int, guard security.SecretGuard) *ToolBuilder { return &ToolBuilder{ store: store, maxPerSession: maxPerSession, guard: guard, } } // ToolDefinition 是一个运行时定义的工具. // Agent 通过对话生成这个结构,然后注册到 Engine 中. type ToolDefinition struct { // Name 工具名称(必须唯一,不能与内置工具冲突) Name string `json:"name"` // Description 工具描述(模型看到的说明) Description string `json:"description"` // InputSchema JSON Schema 格式的输入定义 InputSchema json.RawMessage `json:"input_schema"` // ExecutionType 执行方式 ExecutionType ToolExecType `json:"execution_type"` // Script shell 脚本内容(当 ExecutionType == ExecScript) // 输入参数通过环境变量传递:TOOL_INPUT_ Script string `json:"script,omitempty"` // Command 命令模板(当 ExecutionType == ExecCommand) // 支持 {{.param_name}} 模板语法 Command string `json:"command,omitempty"` // Version 版本号(Agent 迭代改进时递增) Version int `json:"version"` // CreatedAt 创建时间 CreatedAt time.Time `json:"created_at"` // CreatedBy 创建者(会话 ID 或 Agent ID) CreatedBy string `json:"created_by,omitempty"` // Rationale Agent 为什么创建这个工具 Rationale string `json:"rationale,omitempty"` // Tags 标签(用于搜索和分类) Tags []string `json:"tags,omitempty"` // ConcurrencySafe 是否可并发执行 ConcurrencySafe bool `json:"concurrency_safe"` // ReadOnly 是否只读 ReadOnly bool `json:"read_only"` } // ToolExecType 是工具执行方式. type ToolExecType string const ( // ExecScript 通过 shell 脚本执行 // Agent 编写完整的 bash 脚本,参数通过环境变量传入 ExecScript ToolExecType = "script" // ExecCommand 通过命令模板执行 // Agent 编写带占位符的命令,引擎替换参数后执行 ExecCommand ToolExecType = "command" // ExecComposite 组合现有工具 // Agent 定义一个工具链(调用多个现有工具的序列) ExecComposite ToolExecType = "composite" ) // Apply 执行工具创建提案. func (tb *ToolBuilder) Apply(ctx context.Context, proposal *EvolutionProposal) error { if tb.created >= tb.maxPerSession { return fmt.Errorf("tool_builder: session limit reached (%d/%d)", tb.created, tb.maxPerSession) } data, err := json.Marshal(proposal.Content) if err != nil { return fmt.Errorf("tool_builder: marshal content: %w", err) } var def ToolDefinition if err := json.Unmarshal(data, &def); err != nil { return fmt.Errorf("tool_builder: unmarshal definition: %w", err) } // 验证 if err := tb.validate(&def); err != nil { return fmt.Errorf("tool_builder: validate: %w", err) } // 持久化 if err := tb.save(&def); err != nil { return fmt.Errorf("tool_builder: save: %w", err) } tb.created++ return nil } // validate 验证工具定义的合法性. func (tb *ToolBuilder) validate(def *ToolDefinition) error { if def.Name == "" { return fmt.Errorf("name is required") } if strings.ContainsAny(def.Name, " \t\n/\\") { return fmt.Errorf("name contains invalid characters") } if def.Description == "" { return fmt.Errorf("description is required") } // 禁止覆盖内置工具名 builtins := map[string]bool{ "Bash": true, "Read": true, "Edit": true, "Write": true, "Glob": true, "Grep": true, "Agent": true, "WebFetch": true, "WebSearch": true, "ToolSearch": true, } if builtins[def.Name] { return fmt.Errorf("cannot override built-in tool %q", def.Name) } switch def.ExecutionType { case ExecScript: if def.Script == "" { return fmt.Errorf("script is required for script execution type") } case ExecCommand: if def.Command == "" { return fmt.Errorf("command is required for command execution type") } case ExecComposite: // ExecComposite 尚未实现(execute 路径明确报错),validate 应与 execute 保持一致: // 让调用方在注册阶段就失败,而不是通过验证后在运行时才报错. return fmt.Errorf("composite execution type is not yet implemented") default: return fmt.Errorf("unknown execution type: %s", def.ExecutionType) } return nil } // save 持久化工具定义. func (tb *ToolBuilder) save(def *ToolDefinition) error { if def.CreatedAt.IsZero() { def.CreatedAt = time.Now() } data, err := json.MarshalIndent(def, "", " ") if err != nil { return err } path := filepath.Join(tb.store.dir, "tools", def.Name+".json") // 精妙之处(CLEVER): 扫描 Script/Command 字段(高风险)-- // Agent 生成的脚本可能将环境变量值(如 ANTHROPIC_API_KEY)硬编码进脚本体. // 扫描整个序列化 JSON(而非只扫 Script 字段),因为 Command/Rationale 也可能含敏感信息. // 在写入磁盘前拦截,避免 key 以明文存在于 ~/.flyto/evolution/tools/ 目录. if tb.guard != nil { matches, err := tb.guard.Scan(path, string(data)) if err != nil && err != security.ErrContentTooLarge { return fmt.Errorf("tool_builder: secret scan failed: %w", err) } if len(matches) > 0 { labels := strings.Join(security.MatchLabels(matches), ", ") return fmt.Errorf("tool_builder: secret detected in tool %q definition — %s — cannot save", def.Name, labels) } } return os.WriteFile(path, data, 0644) } // LoadAll 加载所有已持久化的工具定义. // Engine 启动时调用,恢复之前创建的工具. func (tb *ToolBuilder) LoadAll() ([]*ToolDefinition, error) { dir := filepath.Join(tb.store.dir, "tools") entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var defs []*ToolDefinition for _, entry := range entries { if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" { continue } data, err := os.ReadFile(filepath.Join(dir, entry.Name())) if err != nil { continue } var def ToolDefinition if err := json.Unmarshal(data, &def); err != nil { continue } defs = append(defs, &def) } return defs, nil } // RuntimeTool 将 ToolDefinition 转换为可执行的 Tool 接口实现. // 这是 ToolBuilder 和 Engine 之间的桥梁. // // # Executor 依赖 (M1 方案 β 严格 DI) // // RuntimeTool 持 execenv.Executor 用于启动 evolve 脚本/命令子进程. 本地 CLI // 场景由 engine.Config.Executor 传 DefaultExecutor{}, 云端 SaaS 场景由 platform // 层传 sandbox.Backend. 无 fallback, nil 直接 panic (在 NewRuntimeTool 校验). type RuntimeTool struct { def *ToolDefinition cwd string executor execenv.Executor } // NewRuntimeTool 基于定义创建运行时工具. // // executor 参数必填, nil 会 panic. 严格 DI 契约 (M1 方案 β). func NewRuntimeTool(def *ToolDefinition, cwd string, executor execenv.Executor) *RuntimeTool { if executor == nil { panic("evolve.NewRuntimeTool: executor is required (M1 strict DI, no fallback)") } return &RuntimeTool{def: def, cwd: cwd, executor: executor} } // Name 返回工具名. func (t *RuntimeTool) Name() string { return t.def.Name } // Description 返回工具描述. func (t *RuntimeTool) Description(ctx context.Context) string { return t.def.Description } // InputSchema 返回输入 schema. func (t *RuntimeTool) InputSchema() json.RawMessage { return t.def.InputSchema } // Execute 执行工具. func (t *RuntimeTool) Execute(ctx context.Context, input json.RawMessage, progress func(float64, string)) (*ToolResult, error) { switch t.def.ExecutionType { case ExecScript: return t.executeScript(ctx, input) case ExecCommand: return t.executeCommand(ctx, input) case ExecComposite: return nil, fmt.Errorf("composite execution not yet implemented") default: return nil, fmt.Errorf("unknown execution type: %s", t.def.ExecutionType) } } // Metadata 返回工具元数据. func (t *RuntimeTool) Metadata() RuntimeToolMetadata { return RuntimeToolMetadata{ ConcurrencySafe: t.def.ConcurrencySafe, ReadOnly: t.def.ReadOnly, IsEvolved: true, Version: t.def.Version, } } // RuntimeToolMetadata is the metadata returned by RuntimeTool.Metadata(), // intended for the Engine tool registry to read when classifying an // Agent-authored tool (concurrent-safe scheduling, read-only gating, audit // tagging of evolved tools, version tracking for iteration). // // Status (2026-04-21): all 4 fields are dead in scan-baseline because the // adapter that would bridge evolve.RuntimeTool into Engine's tool registry // is NOT wired in core. See engine_integration.go header for the C-plan // status note; short version is "subagent / skill / memory currently cover // reusable-capability needs, C-plan revisits when an industry platform hits // a real RPA-shape workload". Fields stay exported so the adapter work does // not have to reinvent the metadata shape. // // RuntimeToolMetadata 是 RuntimeTool.Metadata() 的返回值, 本意是让 Engine // 工具注册表读取用于: 并发安全调度 / 只读性判断 / 标记"这是进化出来的工具" // 供审计 / 版本追踪迭代. // // 现状 (2026-04-21): 4 字段在 scan-baseline 全 dead, 因为 evolve.RuntimeTool // 到 Engine 工具注册表的适配器没在 core 里接. 见 engine_integration.go 头部 // C 方案状态注释; 简短版: "subagent / skill / memory 当前覆盖能力沉淀需求, // C 方案等行业 platform 遇到真正 RPA 形态的反复任务再接". 字段保留 exported // 让未来 adapter 直接复用此 shape 不用重定义. type RuntimeToolMetadata struct { ConcurrencySafe bool ReadOnly bool IsEvolved bool // 标记这是 Agent 进化出来的工具 Version int } // ToolResult is the execution result from RuntimeTool.Execute -- a simplified // shape kept local to evolve so this package does not import pkg/tools and // create a cycle. // // Status (2026-04-21): both fields are dead in scan-baseline for the same // reason as RuntimeToolMetadata -- the RuntimeTool.Execute → Engine-tool- // registry → model-visible-result path is not wired in core. Tests in // evolve_test.go assert IsError / Output to lock forward-propagation; the // real consumer is the future adapter that translates evolve.ToolResult // into tools.Result for model consumption. // // ToolResult 是 RuntimeTool.Execute 的返回结构, 简化版保留在 evolve 包内避免 // import pkg/tools 形成循环. // // 现状 (2026-04-21): 2 字段在 scan-baseline 标 dead, 原因与 RuntimeToolMetadata // 相同 -- RuntimeTool.Execute → Engine 工具注册表 → 模型可见结果这条路径在 // core 里没接. evolve_test.go 已锁 IsError / Output 的 forward 传递; 真正 // 消费方是未来把 evolve.ToolResult 翻译成 tools.Result 让模型消费的 adapter. type ToolResult struct { Output string IsError bool } // executeScript 通过 shell 脚本执行工具. // // NOTE (2026-04-15 L511 审计闭合 + M1 4d 迁移): 此处走 ClassEvolve + // FullInheritMap, **刻意**全继承 os.Environ(), 和 plugin tool / MCP stdio / // plugin-owned hook 的零信任策略分道扬镳. 不是漏修, 是 L511/L514 反向讨论 // 过的审慎决策 — 完整理由见 pkg/plugin/doc.go 的"Evolve 工具的例外"小节. // TL;DR: evolve 脚本等价于 "用户手敲 bash" (LLM 生成 + ApprovalFunc 人类实时 // 审批), 产品价值依赖读 $GITHUB_TOKEN / $GOOGLE_APPLICATION_CREDENTIALS 等 // env, 本地 Flyto 无进程沙盒前提下 env 白名单只堵 1/10 个洞, 是安全剧场. // 云端 SaaS 场景由 platform sandbox.Backend 接管 ClassEvolve, 在 microVM 里 // 自然没有敏感 env 可继承, 收紧发生在后端而不是 core 的 Env map 构造. 未来 // 审计请勿"顺手"改成 MinimalEnvMap. func (t *RuntimeTool) executeScript(ctx context.Context, input json.RawMessage) (*ToolResult, error) { return t.executeBash(ctx, input, t.def.Script) } // executeCommand 通过命令模板执行工具. // // 精妙之处(CLEVER): 参数通过环境变量传递而非字符串替换,和 ExecScript 模式一致. // 命令模板中用 $TOOL_INPUT_PARAM_NAME 引用参数,shell 负责变量展开. // 这样即使参数值包含 shell 特殊字符(; rm -rf /),也只是一个变量值,不会被执行. // 这个设计借鉴了早期方案 Bash 工具的安全模式:永远不要拼接用户输入到 shell 命令中. // // NOTE (2026-04-15 L511 审计闭合 + M1 4d 迁移): env 继承策略和 executeScript // 相同, 刻意走 FullInheritMap. 完整决策理由见 executeScript 的 doc 注释以及 // pkg/plugin/doc.go 的"Evolve 工具的例外"小节. func (t *RuntimeTool) executeCommand(ctx context.Context, input json.RawMessage) (*ToolResult, error) { return t.executeBash(ctx, input, t.def.Command) } // executeBash 是 executeScript 和 executeCommand 的共享实现. 两者唯一的 // 区别是读 def.Script 还是 def.Command, 其他 (参数 -> env 映射, Class, // WorkDir, 错误路径) 完全一致. 抽出共享函数避免两处分支各自演进出 bug. func (t *RuntimeTool) executeBash(ctx context.Context, input json.RawMessage, bashSource string) (*ToolResult, error) { var params map[string]any if err := json.Unmarshal(input, ¶ms); err != nil { return &ToolResult{Output: "invalid input: " + err.Error(), IsError: true}, nil } // 构建参数 env -- FullInheritMap 负责 os.Environ() 全继承, extra 覆盖同名. // 参数通过环境变量传递 (见 executeCommand 的安全注释, 防 shell 注入). extra := make(map[string]string, len(params)) for k, v := range params { envKey := "TOOL_INPUT_" + strings.ToUpper(strings.ReplaceAll(k, "-", "_")) extra[envKey] = fmt.Sprintf("%v", v) } proc := t.executor.Command(ctx, execenv.Spec{ Class: execenv.ClassEvolve, Path: "bash", Args: []string{"-c", bashSource}, Env: execenv.FullInheritMap(extra), WorkDir: t.cwd, }) output, err := proc.CombinedOutput() if err != nil { return &ToolResult{ Output: string(output) + "\nexit: " + err.Error(), IsError: true, }, nil } return &ToolResult{Output: string(output)}, nil }