// Package tools 的 ToolCapability 协议 -- Agent Tool Safety Protocol. // // 升华改进(ELEVATED): 定义 Agent Tool Safety Protocol. // 文件/数据库/API 三种操作统一为同一套能力声明. // 支持 DryRun 的工具会被 Agent 优先选择(因为更安全). // 替代方案:所有工具一视同仁,不声明能力(无法区分安全等级). // // 三个层次: // // Level 0: 不支持 DryRun 也不可逆 → Agent 调用前警告 // Level 1: Reversible(可回滚)→ Saga 补偿模式 // Level 2: DryRun(可预览)→ 先试再做,最安全 package tools import ( "context" "encoding/json" "fmt" ) // ToolCapability 声明工具的安全能力. type ToolCapability struct { // DryRun 是否支持模拟执行(不产生副作用) DryRun bool // Reversible 执行后是否可撤销 Reversible bool // UndoMethod is an informational static declaration of how this tool // can be undone: "tool" / "manual" / "" (irreversible). This is a // surface for external audit UI / tool panels to show rollback // category before execution; the engine's actual rollback path reads // UndoInfo.ToolName (dynamic, captured by Reversible.GenerateUndo at // execute time), not this field. Static vs dynamic are complementary, // not redundant: static lets the UI render "this tool is reversible" // without running; dynamic carries the concrete undo input per call. // // UndoMethod 是工具如何被撤销的 informational 静态声明: "tool" / "manual" // / "" (不可逆). 此字段面向外部审计 UI / 工具面板, 让其在执行前就能 // 展示回滚分类; 引擎真实 rollback 路径读 UndoInfo.ToolName (动态, 由 // Reversible.GenerateUndo 执行时捕获), 不读本字段. 静态和动态互补非 // 冗余: 静态让 UI 不跑也能渲染 "此工具可撤销"; 动态携带每次调用的 // 具体撤销参数. UndoMethod string // UndoToolName is the informational static name of the undo tool // when UndoMethod="tool". Same static-vs-dynamic split as UndoMethod: // external UI / audit consumers read this field to show "undo is // handled by tool X" pre-execution; the engine dispatches undo via // UndoInfo.ToolName (dynamic, same value in practice but sourced // per-call by GenerateUndo). // // UndoToolName 是 UndoMethod="tool" 时撤销工具的 informational 静态名. // 和 UndoMethod 同构的静态-动态分层: 外部 UI / 审计消费者读本字段 // 在执行前展示 "由工具 X 撤销"; 引擎执行 undo 时派发 UndoInfo.ToolName // (动态值, 通常同值但按次由 GenerateUndo 提供). UndoToolName string // MinConfidence is the minimum self-reported confidence the engine // requires before dispatching this tool (0-100). Zero disables the // gate entirely (all calls pass). // // Wire (2026-04-21): // - engine.buildToolDefs appends a "[Safety] requires _flyto_confidence // in input, min=N" hint to Description for tools with MinConfidence>0, // so the LLM sees the contract in the tool definition itself. // - orchestrator.executeSingle calls CheckConfidenceGate before // Execute. The gate parses the `_flyto_confidence` reserved field // from input JSON, rejects calls below the threshold with a // tool-level error (letting the LLM retry with a reconsidered // value), and strips the field before handing input to the tool // (so tool InputSchema stays clean). // - Missing _flyto_confidence when MinConfidence>0 is treated as // gate-fail (not implicit 100) -- LLM must explicitly self-assess // for high-risk tools. // // MinConfidence 是引擎派发本工具前要求的最低自评置信度 (0-100). 零值 // 完全 disable gate (所有调用放行). // // Wire (2026-04-21): // - engine.buildToolDefs 对 MinConfidence>0 的工具, 在 Description 末 // 尾追加 "[Safety] 需要在 input 中附 _flyto_confidence, 最低 N" 提示, // 让 LLM 在工具定义层就看到契约. // - orchestrator.executeSingle 在 Execute 前调 CheckConfidenceGate. // Gate 从 input JSON 解析保留字段 `_flyto_confidence`, 低于阈值返工具 // 级错误 (让 LLM 重新评估), 放行前剥除该字段 (保持工具 InputSchema // 干净). // - MinConfidence>0 而 _flyto_confidence 缺失视为 gate-fail (非隐式 100) // -- LLM 必须为高风险工具显式自评. MinConfidence int // AffectedResources is a human-readable list of resource categories // the tool touches (e.g. ["file"], ["database","network"]). Pure // informational surface for tool panels / audit UI to classify tools // by blast radius. Engine does not read this field; consumers walk // CapabilityProvider externally. // // AffectedResources 是工具触及的资源类别人类可读列表 (如 ["file"] / // ["database","network"]). 纯 informational, 给工具面板 / 审计 UI 按 // 影响范围分类. 引擎不读本字段, 外部消费者走 CapabilityProvider 调取. AffectedResources []string } // ConfidenceInputField is the reserved JSON key the LLM uses to self-report // tool-call confidence (0-100). Prefix underscore marks it as a framework // field, not a tool business parameter; the orchestrator strips it before // Execute so tool InputSchema contracts stay intact. // // ConfidenceInputField 是 LLM 自报工具调用置信度 (0-100) 的 JSON 保留字段. // 下划线前缀表示这是框架字段而非工具业务参数; orchestrator 在 Execute 前 // 剥除, 工具 InputSchema 契约不被侵入. const ConfidenceInputField = "_flyto_confidence" // CheckConfidenceGate enforces ToolCapability.MinConfidence against the // `_flyto_confidence` reserved field in the input JSON. Returns: // - pass=true, errMsg="", stripped: input with the reserved field removed, // safe to hand to the tool. // - pass=false, errMsg=reason: caller returns a tool-level error. // // Gate semantics: // - cap.MinConfidence == 0: always pass, input returned unchanged (no // parse, no allocation). // - cap.MinConfidence > 0 and field missing: fail with "requires // _flyto_confidence (0-100) in input, min=N". // - cap.MinConfidence > 0 and field < threshold: fail with "confidence M // below required N". // - cap.MinConfidence > 0 and field >= threshold: pass, field stripped. // // Input must be a JSON object. Non-object / invalid JSON returns pass=true // unchanged when gate disabled; when gate enabled, returns fail (cannot // locate the reserved field). // // CheckConfidenceGate 按 ToolCapability.MinConfidence 校验 input JSON 中的 // `_flyto_confidence` 保留字段. 返回: // - pass=true, errMsg="", stripped: 剥除保留字段后的 input, 可直接给工具. // - pass=false, errMsg=理由: 调用方返工具级错误. // // Gate 语义: // - cap.MinConfidence == 0: 永远放行, input 原样返回 (不解析不分配). // - cap.MinConfidence > 0 且字段缺失: 失败 "requires _flyto_confidence // (0-100) in input, min=N". // - cap.MinConfidence > 0 且字段 < 阈值: 失败 "confidence M below // required N". // - cap.MinConfidence > 0 且字段 >= 阈值: 放行, 字段剥除. // // input 必须是 JSON 对象. 非对象 / 无效 JSON: gate disabled 时原样放行; // gate enabled 时视为失败 (无法定位保留字段). func CheckConfidenceGate(cap ToolCapability, input json.RawMessage) (pass bool, errMsg string, stripped json.RawMessage) { if cap.MinConfidence <= 0 { return true, "", input } if len(input) == 0 { return false, fmt.Sprintf("tool requires %s (0-100) in input, min=%d", ConfidenceInputField, cap.MinConfidence), nil } var obj map[string]json.RawMessage if err := json.Unmarshal(input, &obj); err != nil { return false, fmt.Sprintf("tool requires %s (0-100) in input, min=%d (input is not a JSON object)", ConfidenceInputField, cap.MinConfidence), nil } raw, ok := obj[ConfidenceInputField] if !ok { return false, fmt.Sprintf("tool requires %s (0-100) in input, min=%d", ConfidenceInputField, cap.MinConfidence), nil } var conf int if err := json.Unmarshal(raw, &conf); err != nil { return false, fmt.Sprintf("tool %s must be an integer (0-100), min=%d", ConfidenceInputField, cap.MinConfidence), nil } if conf < cap.MinConfidence { return false, fmt.Sprintf("confidence %d below required %d -- reconsider input or explain why you are confident", conf, cap.MinConfidence), nil } delete(obj, ConfidenceInputField) cleaned, err := json.Marshal(obj) if err != nil { return false, fmt.Sprintf("failed to strip %s from input: %v", ConfidenceInputField, err), nil } return true, "", cleaned } // SafetyLevel 返回工具的安全等级. // Level 2: DryRun(最安全),Level 1: Reversible,Level 0: 两者都不支持. func (c ToolCapability) SafetyLevel() int { if c.DryRun { return 2 } if c.Reversible { return 1 } return 0 } // DryRunnable 支持模拟执行的工具(可选接口). // 精妙之处(CLEVER): 用可选接口而非修改 Tool 接口,不破坏现有实现. // 现有的 10+ 个工具都不需要改动,只有需要 DryRun 能力的工具才实现这个接口. // // Shape: synchronous callback. Engine / preview UI calls DryRun to get // a preview of effects without executing; tool author implements to // compute diff / impact without side effects. // // 形态: 同步回调. 引擎 / 预览 UI 调 DryRun 得到效果预览而不执行; 工具作者 // 实现此方法计算 diff / 影响范围而不产生副作用. type DryRunnable interface { // DryRun 模拟执行,返回"会发生什么"但不真正执行 DryRun(ctx context.Context, input json.RawMessage) (*DryRunResult, error) } // DryRunResult is the return shape of DryRunnable.DryRun. All three fields // are external pull-API surface: engine / preview UI / safety dashboard // call DryRun and read them directly. Engine does not internally read these // fields (no in-tree consumer), which is expected -- DryRun is the // pre-execution preview contract, not an internal wire. // // DryRunResult 是 DryRunnable.DryRun 的返回形状. 三个字段均为外部调取 // (pull) API 表面: 引擎 / 预览 UI / 安全面板调 DryRun 后直接读. 引擎内部 // 不读这些字段 (无 in-tree consumer), 属预期行为 -- DryRun 是预执行 // 预览契约, 不走内部 wire. type DryRunResult struct { // WouldAffect is a human-readable description of what will be // affected (e.g. file path, table name, endpoint URL). Consumed by // preview UI to show "this will touch X" before approval. // // WouldAffect 是会影响什么的人类可读描述 (如文件路径 / 表名 / // endpoint URL). 预览 UI 消费用于批准前展示 "即将影响 X". WouldAffect string // Preview is the detailed preview payload (e.g. unified diff, change // summary, SQL plan). Shown to the human operator / presented to the // LLM in an approval loop. // // Preview 是详细预览载荷 (如 unified diff / 变更摘要 / SQL plan). // 展示给人类操作员 / 在审批循环中展示给 LLM. Preview string // EstimatedImpact is a structured extension slot (map[string]any) for // consumer-specific impact metrics -- row counts, byte deltas, // estimated cost, etc. Schema is consumer-defined; engine does not // interpret it. Similar extension shape as ShadowResult.Meta and // ReplayEvent.Meta. // // EstimatedImpact 是结构化扩展槽 (map[string]any), 给消费端特定的 // 影响指标 -- 行数 / byte 增量 / 预估成本等. schema 由消费端定义, // 引擎不解读. 扩展形状同 ShadowResult.Meta / ReplayEvent.Meta. EstimatedImpact map[string]any } // Reversible 可撤销的工具(可选接口). // 精妙之处(CLEVER): Saga 补偿模式--每个正向操作生成对应的补偿操作. // 回滚时倒序执行所有补偿操作,和分布式事务的 Saga 模式一样. // // Shape: synchronous callback. Engine calls GenerateUndo synchronously // after execution to capture an undo record; later the engine can // replay the undo to revert. // // 形态: 同步回调. 引擎在执行后同步调 GenerateUndo 拿撤销记录; 之后引擎 // 可回放撤销做回退. type Reversible interface { // GenerateUndo 基于执行结果生成撤销信息 GenerateUndo(ctx context.Context, input json.RawMessage, result *Result) (*UndoInfo, error) } // UndoInfo is the dynamic undo record captured by Reversible.GenerateUndo // at execute time. ToolName / Input / Irreversible / ManualGuide are read // by the engine's rollback path (OperationLog.RollbackMessage → // UndoExecutor.ExecuteUndo). Description is an external pull-API field: // external rollback UI / audit sinks render it to the operator; no // internal assert site in the engine itself. // // UndoInfo 是 Reversible.GenerateUndo 在执行时捕获的动态撤销记录. // ToolName / Input / Irreversible / ManualGuide 由引擎 rollback 路径 // (OperationLog.RollbackMessage → UndoExecutor.ExecuteUndo) 读取. // Description 是外部调取 (pull) API 字段: 外部 rollback UI / 审计 sink // 渲染给操作员, 引擎内部无断言点. type UndoInfo struct { // ToolName 用哪个工具撤销 ToolName string // Input 撤销工具的输入 Input map[string]any // Description is a human-readable summary of the undo action shown to // external operators (rollback UI / audit logs). The engine does not // read this field during RollbackMessage -- it is purely a consumer // surface. // // Description 是向外部操作员展示的撤销动作人类可读摘要 (rollback UI / // 审计日志). 引擎在 RollbackMessage 过程中不读本字段 -- 纯消费者表面. Description string // Irreversible 标记为不可逆(某些操作执行后无法撤销) Irreversible bool // ManualGuide 不可逆时的人工处理指南 ManualGuide string } // CapabilityProvider 工具能力声明(可选接口). // 精妙之处(CLEVER): 和 MetadataProvider 对称设计-- // Metadata 描述"工具是什么",Capability 描述"工具能做什么安全措施". // // Shape: pull. Engine / audit UI reads Capability() to learn which // resources the tool affects, whether undo is supported, and the // minimum confidence required. // // 形态: 调取 (pull). 引擎 / 审计 UI 读 Capability() 得知工具影响哪些资源 / // 是否支持撤销 / 最低置信度要求. type CapabilityProvider interface { Capability() ToolCapability } // GetCapability 安全获取工具能力. // 如果工具未实现 CapabilityProvider,返回零值(不支持 DryRun,不可逆). func GetCapability(t Tool) ToolCapability { if cp, ok := t.(CapabilityProvider); ok { return cp.Capability() } // 默认:不支持 DryRun,不可逆 return ToolCapability{} } // IsDryRunnable 检查工具是否支持模拟执行. func IsDryRunnable(t Tool) bool { _, ok := t.(DryRunnable) return ok } // IsReversible 检查工具是否可撤销. func IsReversible(t Tool) bool { _, ok := t.(Reversible) return ok }