Documentation
¶
Overview ¶
Package permission - composite_handler.go 实现多处理器叠加的 CompositeHandler.
宪法第 8 条「叠加而非替换」在权限层的实现. 多个权限处理器可以共存:CLI 弹框决策 + 审计日志记录 + WebHook 通知.
使用示例:
cliHandler := func(ctx context.Context, req *Request) (*Response, error) {
// 终端弹框,等待用户输入
return &Response{Decision: DecisionAllow, Reason: "user approved"}, nil
}
auditHandler := func(ctx context.Context, req *Request) (*Response, error) {
// 记审计日志(不参与决策)
log.Printf("permission request: %s", req.ToolName)
return nil, nil
}
combined := NewCompositeHandler(
NamedHandler{Name: "cli", Handler: cliHandler, IsDecisionMaker: true},
NamedHandler{Name: "audit", Handler: auditHandler, IsDecisionMaker: false},
)
// 权限请求同时弹框给用户 + 记录审计日志
执行策略:
- 所有 Handler 都会被调用(审计需要)
- 第一个返回 Allow/Deny 的决策者 Handler 决定最终结果
- 观察者 Handler 仍然执行,但不影响决策
- 如果所有决策者都返回 nil/error,默认 Deny
升华改进(ELEVATED): 多渠道权限处理.CLI,HTTP,Hook,审计可以同时工作. 替代方案:单个 Handler(原始设计,只支持一种交互方式).
Package permission is the permission subsystem: the consumer-implementable Handler for Allow/Deny/Ask decisions, the Checker that orchestrates it with rules / classifiers / denial tracking, and the AI-based SecurityClassifier for automatic classification of tool calls.
Consumer integration surfaces fall into three shapes. For the full taxonomy see `docs/api-reference.md` section "API 消费形态 / API Consumption Patterns".
Synchronous callback — form 3:
- Handler (func type): consumer-supplied callback for permission decisions; injected via engine.Config.PermissionHandler
- SecurityClassifier.Classify: consumers can plug an alternate classifier backend; engine or a Handler calls Classify synchronously
Pull — form 2:
- DenialTracker.Stats() DenialStats: cumulative denial snapshot for UI / audit / circuit-breaker decisions
- Classifier.Classify returns *ClassifyResult as a pull-side payload (consumer reads Decision / Reason / Thinking / Stage / Usage / DurationMs to drive UX)
Package permission 是权限子系统: 消费者可实现的 Handler 用于 Allow/Deny/Ask 决策, Checker 负责编排规则 / 分类器 / 拒绝追踪, AI 的 SecurityClassifier 做工具调用的自动分类.
消费者接入面分三种形态. 完整分类见 `docs/api-reference.md` "API 消费形态 / API Consumption Patterns" 章节.
同步回调 (callback) —— 形态三:
- Handler (函数类型): 消费者回调, 经 engine.Config.PermissionHandler 注入
- SecurityClassifier.Classify: 消费者可换 classifier 后端; 引擎或 Handler 同步调 Classify
调取 (pull) —— 形态二:
- DenialTracker.Stats() DenialStats: 累计拒绝快照, 供 UI / 审计 / 熔断决策消费
- Classifier.Classify 返回的 *ClassifyResult 亦作 pull payload (消费者 读 Decision / Reason / Thinking / Stage / Usage / DurationMs 驱动 UX)
Package permission 定义权限引擎的接口和实现.
权限系统的核心实现. src/hooks/toolPermission/ 的功能.
原项目的问题:
- 权限检查逻辑分散在工具内部(tool.checkPermissions)和外部(hasPermissionsToUseTool)两层
- 规则匹配嵌套极深,compound 命令分解逻辑和主流程混在一起
- 权限 UI(React 组件)和权限逻辑强耦合
Go 版本的设计:
- 权限引擎是纯逻辑,不依赖任何 UI
- Handler 接口由消费层实现(CLI 弹对话框,SDK 返回 JSON,HTTP 用 WebSocket)
- 规则匹配是独立函数,可单元测试
Index ¶
- Constants
- Variables
- func CompactToolUse(toolName string, input map[string]any) string
- func DetectProvider(modelID string) string
- func ExplainPermissionRequest(toolName string, input map[string]any) string
- func ExtractCommandName(cmd string) (command string, subcommand string)
- func ExtractDomainFromURL(url string) string
- func ExtractFilePath(input map[string]any) string
- func ExtractURL(input map[string]any) string
- func GetCommandPrefixes(cmd string) []string
- func GlobMatch(pattern, path string) bool
- func IsDangerousCommand(cmd string) bool
- func IsDangerousCommandName(command string) bool
- func IsDangerousPath(path string) bool
- func IsProtectedDir(path string) bool
- func MatchDomain(ruleDomain, actualDomain string) bool
- func MergeWhitelist(base map[string]bool, extra []string) map[string]bool
- func RegisterClassifierFactory(provider string, factory ClassifierFactory)
- func RegisterToolClass(toolName string, class PermissionClass)
- func SerializeRule(rule Rule) string
- func SourcePriority(source RuleSource) int
- func SplitCompoundCommand(cmd string) []string
- func UnregisterToolClass(toolName string)
- type AIClassifier
- type Checker
- type ClassifierFactory
- type ClassifierUsage
- type ClassifyRequest
- type ClassifyResult
- type CompositeHandler
- type ContentType
- type DangerInfo
- type Decision
- type DenialStats
- type DenialTracker
- func (dt *DenialTracker) RecordApproval(toolName string)
- func (dt *DenialTracker) RecordDenial(toolName, inputSummary string)
- func (dt *DenialTracker) Reset()
- func (dt *DenialTracker) ShouldStopAndAsk() bool
- func (dt *DenialTracker) ShouldWarnAgent() (warn bool, message string)
- func (dt *DenialTracker) Stats() DenialStats
- type DenyRule
- type Handler
- type HybridClassifier
- type LearningStats
- type LearningTracker
- type Mode
- type NamedHandler
- type ParsedContent
- type PermissionClass
- type PermissionDedup
- type Request
- type Response
- type RiskLevel
- type Rule
- type RuleApplier
- type RuleDenyEngine
- type RuleSource
- type SecurityClassifier
- func NewAnthropicClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier
- func NewClassifierForProvider(providerName string, provider flyto.ModelProvider, ...) SecurityClassifier
- func NewClassifierFromRegistry(registry *config.ModelRegistry, provider flyto.ModelProvider) SecurityClassifier
- func NewGenericClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier
- func NewGoogleClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier
- func NewOpenAIClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier
- type SedCheckResult
- type SuggestedRule
- type TranscriptEntry
Constants ¶
const DefaultPermissionTimeout = 5 * time.Minute
DefaultPermissionTimeout 是 Handler 调用的默认超时时间.
精妙之处(CLEVER): 5 分钟是"用户能够在终端做出响应"的经验上界-- 用户在 CLI 看到权限请求后,通常秒级响应;极少数场景(开会,步骤复杂)也不超过 5 分钟. 超过 5 分钟的 Handler 阻塞几乎必然是 bug(deadlock,网络挂死),而不是正常等待. 替代方案:不超时(早期设计)--Handler 永久挂死时 runLoop 无法退出,进程只能被 kill.
const MaxSubcommands = 50
MaxSubcommands 是单条 Bash 输入允许的最大子命令数量.
精妙之处(CLEVER): 50 子命令上限不是性能优化,是安全防线. 恶意注入可以构造 cmd1; cmd2; ... cmd1000 的超长命令串, 逐个检查时分类器调用次数爆炸.50 是"正常不会超过,恶意会超过"的经验值. 正常 shell one-liner 一般 5-15 个子命令.
Variables ¶
var BuiltinDenyRules = []DenyRule{
{Pattern: "rm -rf /", Reason: "destructive: removes root filesystem", Category: "command"},
{Pattern: "rm -rf ~", Reason: "destructive: removes home directory", Category: "command"},
{Pattern: "rm -rf /*", Reason: "destructive: removes all root entries", Category: "command"},
{Pattern: "> /dev/sda", Reason: "destructive: writes to raw disk", Category: "command"},
{Pattern: "> /dev/nvme", Reason: "destructive: writes to raw disk", Category: "command"},
{Pattern: "mkfs.", Reason: "destructive: formats filesystem", Category: "command"},
{Pattern: "dd if=", Reason: "potentially destructive: raw disk operation", Category: "command"},
{Pattern: "dd of=/dev/", Reason: "destructive: writes to raw device", Category: "command"},
{Pattern: ":(){ :|:& };:", Reason: "fork bomb", Category: "command"},
{Pattern: ":(){:|:&};:", Reason: "fork bomb (no spaces)", Category: "command"},
{Pattern: "curl -d @/etc/passwd", Reason: "data exfiltration: sends passwd file", Category: "network"},
{Pattern: "curl -T /etc/shadow", Reason: "data exfiltration: uploads shadow file", Category: "network"},
{Pattern: "chmod 4755", Reason: "setuid bit: potential privilege escalation", Category: "command"},
{Pattern: "chmod u+s", Reason: "setuid bit: potential privilege escalation", Category: "command"},
{Pattern: "> /etc/passwd", Reason: "destructive: overwrites passwd file", Category: "file"},
{Pattern: "> /etc/shadow", Reason: "destructive: overwrites shadow file", Category: "file"},
{Pattern: "system(", Reason: "code execution: awk/perl system() call", Category: "command", ToolName: "Bash"},
{Pattern: "| getline", Reason: "code execution: awk pipe to getline", Category: "command", ToolName: "Bash"},
{Pattern: "-exec ", Reason: "code execution: find -exec runs commands on matched files", Category: "command", ToolName: "Bash"},
{Pattern: "-execdir ", Reason: "code execution: find -execdir runs commands in file's directory", Category: "command", ToolName: "Bash"},
{Pattern: "| xargs ", Reason: "code execution: xargs builds and executes commands from input", Category: "command", ToolName: "Bash"},
}
BuiltinDenyRules 内置 deny 规则(编译进引擎).
历史包袱(LEGACY): 这些规则来源于多年的安全事件积累, 每一条背后都有一个"差点出事"的故事. 例如 `:(){:|:&};:` 是 fork bomb,曾经有 Agent 在尝试解释 shell 语法时意外执行了它.
var DefaultSafeTools = map[string]bool{ "Read": true, "Grep": true, "Glob": true, "ToolSearch": true, "TaskCreate": true, "TaskGet": true, "TaskList": true, "TaskUpdate": true, "WebSearch": true, }
DefaultSafeTools 是默认的安全工具白名单. 包含所有只读工具和不会修改系统状态的操作.
精妙之处(CLEVER): 白名单的工具选择标准是"即使被恶意调用也不会造成损害"-- 读文件,搜索代码,列任务这些操作最多泄露信息(这本来就是 Agent 需要的), 但不会修改或删除任何东西.信息泄露由对话隔离保证,不在分类器的职责范围内.
Functions ¶
func CompactToolUse ¶
CompactToolUse 将工具调用压缩为一行.
格式:ToolName: <关键参数> 例如:
- Bash: ls -la /home
- Edit: /src/main.go (old_string -> new_string)
- Read: /src/config.go
- Grep: pattern="TODO" path=/src
func DetectProvider ¶
DetectProvider 根据模型 ID 推断提供商.
精妙之处(CLEVER): 通过模型 ID 前缀自动检测提供商, 用户不需要显式配置 provider 字段. claude-* → anthropic, gpt-* → openai, gemini-* → google.
func ExplainPermissionRequest ¶
ExplainPermissionRequest 为权限请求生成人类可读的说明.
根据工具类型和输入参数,生成清晰的操作描述,帮助用户理解:
- 将要执行什么操作
- 操作的具体内容(命令,文件路径,URL 等)
- 为什么需要授权
func ExtractCommandName ¶
ExtractCommandName 从命令字符串中提取主命令名和子命令.
使用 AST 解析器正确提取命令名,跳过 env/sudo 等前缀. 保持原有的函数签名,内部实现替换为 AST 解析.
会跳过常见的前缀:
- env VAR=value ...
- sudo ...
- nohup ...
- time ...
- nice ...
示例:
- "npm install" → ("npm", "install")
- "env FOO=bar npm i" → ("npm", "i")
- "sudo rm -rf /" → ("rm", "-rf")
- "git push --force" → ("git", "push")
func ExtractDomainFromURL ¶
ExtractDomainFromURL 从 URL 中提取域名.
示例:
- "https://example.com/path" → "example.com"
- "http://api.github.com:443/v1" → "api.github.com"
func GetCommandPrefixes ¶
GetCommandPrefixes 从命令字符串中提取所有可能的前缀用于规则匹配.
对于 "npm install lodash",返回 ["npm", "npm install", "npm install lodash"]
func GlobMatch ¶
GlobMatch 执行路径 glob 匹配.
支持的通配符:
- * 匹配路径段内的任意字符(不跨越 /)
- ** 匹配任意层级的路径(跨越 /)
- ? 匹配单个字符
示例:
- GlobMatch("/src/**", "/src/app/main.go") → true
- GlobMatch("/src/*.go", "/src/main.go") → true
- GlobMatch("/src/*.go", "/src/sub/main.go") → false
func IsDangerousCommand ¶
IsDangerousCommand 检查命令是否为危险命令.
检查维度:
- 命令名是否在危险命令列表中
- 命令+参数组合是否匹配危险模式
- 命令内容是否包含危险 SQL 语句
- 命令是否操作危险文件
- 命令替换 $(...) 或反引号内的嵌套命令(递归检查)
升华改进(ELEVATED): 使用 ExtractAllCommands 递归提取命令替换内的命令, 防止 `echo $(rm -rf /)` 这类嵌套危险命令绕过检测. 早期方案只检查顶层命令,命令替换内部命令不可见. 替代方案:只对字符串做全文匹配--否决:高误报率,且无法正确提取命令名/参数.
func IsDangerousCommandName ¶
IsDangerousCommandName 检查命令名是否在已知危险命令列表中.
func IsDangerousPath ¶
IsDangerousPath 检查路径是否为危险路径.
危险路径包括:
- Shell 配置文件(.bashrc 等)
- Git hooks
- SSH 密钥和配置
- 系统关键文件
func MatchDomain ¶
MatchDomain 检查域名是否匹配规则域名.
支持子域名匹配:
- "example.com" 匹配 "example.com" 和 "sub.example.com"
- "api.github.com" 只匹配 "api.github.com"
精妙之处(CLEVER): 域名匹配支持子域名自动包含--规则 "example.com" 同时匹配 "example.com" 和 "sub.example.com",符合域名所有权的直觉. 但 "api.github.com" 不会匹配 "github.com"(子域名不能反向匹配父域名).
func MergeWhitelist ¶
MergeWhitelist 合并用户自定义的安全工具到白名单. 返回合并后的新白名单(不修改原始 map).
func RegisterClassifierFactory ¶
func RegisterClassifierFactory(provider string, factory ClassifierFactory)
RegisterClassifierFactory 注册自定义的分类器工厂. 并发安全,可在插件热加载场景中从多 goroutine 调用.
func RegisterToolClass ¶
func RegisterToolClass(toolName string, class PermissionClass)
RegisterToolClass 注册工具的权限检查类型. 通常由 tools.Registry 在注册工具时自动调用,第三方工具也可手动调用.
精妙之处(CLEVER): 解耦 permission ↔ tools 双向依赖-- permission 包不 import tools,tools.Registry 调用此函数注入映射. 工具新增时只需在 Metadata.PermissionClass 声明一次,无需改权限核心代码.
func SerializeRule ¶
SerializeRule 将规则序列化为字符串.
示例:
- Rule{ToolName: "Bash", Content: "prefix:npm"} → "Bash(prefix:npm)"
- Rule{ToolName: "*", Content: ""} → "*"
func SourcePriority ¶
func SourcePriority(source RuleSource) int
SourcePriority 返回规则来源的优先级数值. 数值越大,优先级越高.
func SplitCompoundCommand ¶
SplitCompoundCommand 将复合 Shell 命令分割为独立的子命令.
使用 AST 解析器正确处理复杂语法:heredoc,嵌套引号,命令替换等. 保持原有的函数签名,内部实现替换为 AST 解析.
支持的分隔符:
- && 逻辑与
- || 逻辑或
- | 管道
- ; 顺序执行
会正确处理引号内的分隔符(不分割).
示例:
- "npm install && npm test" → ["npm install", "npm test"]
- "echo 'a && b'" → ["echo 'a && b'"](引号内不分割)
- "cat file | grep pattern | wc -l" → ["cat file", "grep pattern", "wc -l"]
精妙之处(CLEVER): 用完整的 AST 解析器做命令分割,而非简单的字符串 split-- 早期方案用正则和字符状态机分割,遇到 heredoc,嵌套引号,命令替换就出错. 例如 `echo "a && b" && rm -rf /` 中 echo 参数里的 && 不应被当作分隔符. 升级为 AST 解析后彻底解决了此类误判,安全检查不再有绕过风险.
func UnregisterToolClass ¶
func UnregisterToolClass(toolName string)
UnregisterToolClass 从动态注册表中移除工具(测试清理用).
Types ¶
type AIClassifier ¶
type AIClassifier struct {
// contains filtered or unexported fields
}
AIClassifier 是 AI 安全分类器. 通过调用 LLM API 判断工具调用是否安全. 精妙之处(CLEVER): 内置决策缓存--同一 toolName+input 组合在 TTL 内复用决策, 避免重复调用 LLM.典型场景:用户连续编辑同一文件,每次都触发相同的安全评估.
func NewAIClassifier ¶
func NewAIClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) *AIClassifier
NewAIClassifier 创建 AI 分类器. stage1Model 用于快速判断(RoleFast),stage2Model 用于深度推理(RoleMain).
func (*AIClassifier) Classify ¶
func (c *AIClassifier) Classify(ctx context.Context, req *ClassifyRequest) (*ClassifyResult, error)
Classify 执行两阶段 AI 分类.
type Checker ¶
type Checker interface {
// Check 检查是否有权限执行工具.
// 根据规则和模式返回决策,如果是 Ask 则调用 Handler.
Check(ctx context.Context, req *Request) (*Response, error)
// Mode 返回当前权限模式.
Mode() Mode
// SetMode 设置权限模式.
SetMode(mode Mode)
// AddRule 添加权限规则.
AddRule(rule Rule)
}
Checker 是权限引擎接口.
升华改进(ELEVATED): 原名 Engine 与 pkg/flyto.Engine(公共顶层接口)同名-- 在同时导入两个包的文件(如 engine.go)里,permission.Engine vs flyto.Engine 极易混淆. 改为 Checker 更准确地描述职责(核心方法是 Check),同时消除命名冲突. 替代方案:<保留 Engine 名> - 否决:同名不同语义是常见 bug 来源, 代码审查中肉眼难以区分是哪个包的 Engine.
type ClassifierFactory ¶
type ClassifierFactory func(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier
ClassifierFactory 是 AI 分类器的工厂函数类型.
type ClassifierUsage ¶
ClassifierUsage 记录分类器的 token 使用情况.
type ClassifyRequest ¶
type ClassifyRequest struct {
ToolName string // 工具名称
ToolInput map[string]any // 工具输入参数
Transcript []TranscriptEntry // 对话历史投影
UserIntent string // FLYTO.md 内容(用户意图)
}
ClassifyRequest 是分类器的输入请求.
type ClassifyResult ¶
type ClassifyResult struct {
Decision Decision // Allow / Deny
Reason string // 原因(Stage 2 时有)
Thinking string // 推理过程
Stage string // "whitelist" / "rule" / "ai_stage1" / "ai_stage2"
Usage *ClassifierUsage // token 使用统计
DurationMs int64 // 耗时(毫秒)
}
ClassifyResult 是分类器的输出结果.
type CompositeHandler ¶
type CompositeHandler struct {
// contains filtered or unexported fields
}
CompositeHandler 将多个权限处理器叠加.
使用示例:
ch := NewCompositeHandler(
NamedHandler{Name: "cli", Handler: cliHandler, IsDecisionMaker: true},
NamedHandler{Name: "webhook", Handler: webhookHandler, IsDecisionMaker: false},
)
// 作为 permission.Handler 使用:
engine := permission.NewEngine(permission.ModeDefault, ch.Handle)
func NewCompositeHandler ¶
func NewCompositeHandler(handlers ...NamedHandler) *CompositeHandler
NewCompositeHandler 创建叠加处理器.
使用示例:
// 单处理器退化--行为与直接使用该 Handler 一致
ch := NewCompositeHandler(NamedHandler{
Name: "cli", Handler: myHandler, IsDecisionMaker: true,
})
// 多处理器叠加--CLI 决策 + 审计日志
ch := NewCompositeHandler(
NamedHandler{Name: "cli", Handler: cliHandler, IsDecisionMaker: true},
NamedHandler{Name: "audit", Handler: auditHandler, IsDecisionMaker: false},
)
func (*CompositeHandler) Add ¶
func (ch *CompositeHandler) Add(h NamedHandler)
Add 动态添加处理器(运行时扩展).
使用示例:
ch.Add(NamedHandler{Name: "webhook", Handler: webhookFn, IsDecisionMaker: false})
Add 动态添加处理器. 如果已有同名处理器,替换(upsert 语义).
func (*CompositeHandler) Handle ¶
Handle 执行所有处理器并返回决策结果. 实现 permission.Handler 签名,可直接传给 NewEngine.
精妙之处(CLEVER): 分离决策者和观察者. 决策者(CLI 弹框)的响应决定最终结果. 观察者(审计日志)的响应被忽略但仍然执行. 这样审计永远不会被跳过,即使决策已经做出.
func (*CompositeHandler) Remove ¶
func (ch *CompositeHandler) Remove(name string) bool
Remove 按名称移除处理器.返回是否成功移除.
使用示例:
ch.Remove("webhook")
type ContentType ¶
type ContentType string
ContentType 是规则内容的匹配类型.
const ( ContentNone ContentType = "" // 无内容条件,匹配该工具的所有调用 ContentPrefix ContentType = "prefix" // 前缀匹配(Bash 命令) ContentPath ContentType = "path" // 路径 glob 匹配(文件操作工具) ContentDomain ContentType = "domain" // 域名匹配(WebFetch 工具) )
type DangerInfo ¶
type DangerInfo struct {
Reason string // 人类可读的风险描述(英文,日志友好)
Pattern string // 匹配到的具体模式(如 "rm -rf" / "drop table" / ".ssh/authorized_keys")
HeredocBodyStart int // Heredoc body 在原文的起始字节 (仅 heredoc 归因时填). EN: origin-source start byte of offending heredoc body.
HeredocBodyEnd int // Heredoc body 在原文的结束字节 (半开). EN: origin-source end byte (exclusive).
}
DangerInfo 结构化的危险分析结果. 供 checkpoint_suggested 事件携带,让消费层展示具体风险原因.
HeredocBodyStart / HeredocBodyEnd 仅当危险字面量归因到某个 heredoc body 时填非零值, 记录该 body 在**原文**中的字节半开区间 [Start, End). 审计 / TUI / 日志可据此精确指向用户输入的触发段 (例 "source bytes 42-67"), 而不只是 "heredoc 里有危险".
HeredocBodyStart / HeredocBodyEnd are non-zero only when the dangerous literal is attributed to a heredoc body; they carry the body's byte half-open interval [Start, End) within the ORIGINAL source. Lets audit / TUI / logs point precisely at the user-input segment that triggered the warning (e.g. "source bytes 42-67"), rather than just "there is danger in a heredoc".
func AnalyzeDanger ¶
func AnalyzeDanger(cmd string) (bool, DangerInfo)
AnalyzeDanger 对单条命令进行结构化危险分析,返回 (危险, DangerInfo).
与 IsDangerousCommand 相比,额外返回匹配的具体模式和可读原因, 用于 checkpoint_suggested 事件中向消费层展示上下文.
升华改进(ELEVATED): 早期设计只返回 bool,消费层无法告知用户"为什么这个命令危险". 结构化分析让 TUI 可以展示 "detected: rm -rf recursive delete" 而非仅"高风险". 替代方案:<返回 string 原因> - 否决:Pattern 和 Reason 职责不同,分开字段更灵活.
升华改进(ELEVATED): 使用 ExtractAllCommands 递归提取命令替换内的嵌套命令, 对每条提取到的命令独立做危险分析,防止 $(rm -rf /) 嵌套绕过. 替代方案:只分析顶层命令--否决:命令替换是最常见的安全绕过手段.
type DenialStats ¶
type DenialStats struct {
ConsecutiveDenials int // 连续拒绝同一工具的次数
TotalDenials int // 总拒绝次数
LastDeniedTool string // 最后被拒绝的工具
LastDeniedInput string // 最后被拒绝的输入摘要
}
DenialStats 是拒绝追踪器的统计信息.
type DenialTracker ¶
type DenialTracker struct {
// contains filtered or unexported fields
}
DenialTracker 追踪权限拒绝事件.
func NewDenialTracker ¶
func NewDenialTracker(consecutiveThreshold, totalThreshold int) *DenialTracker
NewDenialTracker 创建一个新的拒绝追踪器.
consecutiveThreshold 为连续拒绝警告阈值(<=0 使用默认值 3). totalThreshold 为总拒绝停止阈值(<=0 使用默认值 10).
func (*DenialTracker) RecordApproval ¶
func (dt *DenialTracker) RecordApproval(toolName string)
RecordApproval 记录一次权限批准事件.
当用户批准了任何工具的调用时调用此方法. 批准后重置连续拒绝计数(表示用户意图已改变). 精妙之处(CLEVER): 任何工具被批准都重置连续拒绝计数--表示用户意图已经改变. 用户可能先拒绝了 3 次 rm,然后批准了 mkdir,说明用户仍在参与决策, 不应该继续认为用户在"连续拒绝"同类操作.
func (*DenialTracker) RecordDenial ¶
func (dt *DenialTracker) RecordDenial(toolName, inputSummary string)
RecordDenial 记录一次权限拒绝事件.
toolName 是被拒绝的工具名称,inputSummary 是输入内容的简短摘要. 如果连续拒绝同一工具,连续计数递增;否则重置连续计数.
func (*DenialTracker) ShouldStopAndAsk ¶
func (dt *DenialTracker) ShouldStopAndAsk() bool
ShouldStopAndAsk 检查是否应该停下来询问用户.
当总拒绝次数达到阈值时,返回 true. Agent 应该停下来直接问用户想怎么做,而不是继续尝试.
func (*DenialTracker) ShouldWarnAgent ¶
func (dt *DenialTracker) ShouldWarnAgent() (warn bool, message string)
ShouldWarnAgent 检查是否应该向 Agent 发出警告.
当连续拒绝同一工具达到阈值时,返回 true 和警告消息. Agent 应该在下次工具调用前收到此消息,换一种方式完成任务.
func (*DenialTracker) Stats ¶
func (dt *DenialTracker) Stats() DenialStats
Stats returns a snapshot of the denial tracker's current state.
Shape: pull. Consumer calls Stats() anytime to read the current cumulative denial state; the four DenialStats fields (ConsecutiveDenials / TotalDenials / LastDeniedTool / LastDeniedInput) are the payload.
Stats 返回拒绝追踪器当前状态的快照.
形态: 调取 (pull). 消费者任意时刻调 Stats() 读当前累计拒绝状态, 四字段 (ConsecutiveDenials / TotalDenials / LastDeniedTool / LastDeniedInput) 即 payload.
type DenyRule ¶
type DenyRule struct {
Pattern string // 匹配模式(在工具输入中搜索)
Reason string // 拒绝原因
Category string // 分类(command / file / network)
ToolName string // 限定工具名(空表示匹配所有工具)
}
DenyRule 是一条 deny 规则.
type Handler ¶
Handler 是权限处理器接口. 消费层实现此接口来处理权限请求.
CLI 消费层:弹出终端对话框,等待用户输入 SDK 消费层:通过 control_request JSON 协议转发 HTTP 消费层:通过 WebSocket 推送到前端 测试:直接返回 Allow
Shape: synchronous callback. Engine passes *Request and blocks for *Response (Decision = Allow / Deny / Ask + optional UpdatedInput rewrite).
形态: 同步回调. 引擎传 *Request 阻塞等 *Response (Decision = Allow / Deny / Ask + 可选 UpdatedInput 改写).
type HybridClassifier ¶
type HybridClassifier struct {
// contains filtered or unexported fields
}
HybridClassifier 混合分类器 - 白名单 + 规则 + AI.
升华改进(ELEVATED): 三层分类器各司其职,不互相越权. 白名单只 allow,规则只 deny,AI 做最终裁决. 这种分离确保每一层的逻辑都是单向的,不会出现冲突. 替代方案:单一 AI 分类器处理所有请求(延迟高,成本高,无法离线工作).
func NewHybridClassifier ¶
func NewHybridClassifier(whitelist map[string]bool, rules []DenyRule, aiClassifier SecurityClassifier) *HybridClassifier
NewHybridClassifier 创建混合分类器. whitelist 为安全工具白名单,rules 为 deny 规则,factory 为 AI 分类器工厂. 如果 whitelist 为 nil,使用 DefaultSafeTools.
func (*HybridClassifier) Classify ¶
func (h *HybridClassifier) Classify(ctx context.Context, req *ClassifyRequest) (*ClassifyResult, error)
Classify 执行混合分类.
流程:
- 白名单检查 → 命中直接 allow
- 规则引擎检查 → 命中直接 deny
- AI 分类器 → 两阶段判断
type LearningStats ¶
type LearningStats struct {
TotalDecisions int // 总决策次数
TotalAllowed int // 总允许次数
TotalDenied int // 总拒绝次数
}
LearningStats 是学习追踪器的统计信息.
type LearningTracker ¶
type LearningTracker struct {
// contains filtered or unexported fields
}
LearningTracker 追踪权限决策历史,用于建议永久规则.
func NewLearningTracker ¶
func NewLearningTracker(threshold int) *LearningTracker
NewLearningTracker 创建一个新的权限学习追踪器.
threshold 指定建议阈值,即同类操作被允许多少次后建议添加永久规则. 传入 0 使用默认值(3 次).
func (*LearningTracker) Reset ¶
func (lt *LearningTracker) Reset()
Reset 重置所有追踪数据. 通常在会话结束或用户显式请求时调用.
func (*LearningTracker) SuggestPermanentRules ¶
func (lt *LearningTracker) SuggestPermanentRules() []SuggestedRule
SuggestPermanentRules 基于历史决策建议永久规则.
返回所有满足建议阈值且未曾建议过的规则. 每条规则只会被建议一次,调用此方法后会标记已建议的规则.
func (*LearningTracker) TrackDecision ¶
func (lt *LearningTracker) TrackDecision(toolName string, input map[string]any, decision Decision)
TrackDecision 记录一次权限决策.
在用户做出允许或拒绝决策后调用此方法,系统将提取操作特征并累计计数. 只记录用户主动做出的决策(Allow 和 Deny),自动决策不记录.
type NamedHandler ¶
type NamedHandler struct {
// Name 处理器名称(用于日志和动态移除)
Name string
// Handler 实际的权限处理函数
Handler Handler
// IsDecisionMaker 是否参与决策.
// true: 该处理器的响应可以决定最终结果(如 CLI 弹框)
// false: 仅观察/审计,响应被忽略但仍然执行
IsDecisionMaker bool
}
NamedHandler 是带名称和角色标记的权限处理器.
使用示例:
nh := NamedHandler{
Name: "cli-prompt",
Handler: myCliHandler,
IsDecisionMaker: true, // 参与决策
}
audit := NamedHandler{
Name: "audit-log",
Handler: myAuditHandler,
IsDecisionMaker: false, // 仅观察,不参与决策
}
type ParsedContent ¶
type ParsedContent struct {
Type ContentType // 内容类型
Value string // 匹配值(前缀字符串,glob 模式,域名)
}
ParsedContent 是解析后的规则内容.
func ParseContent ¶
func ParseContent(content string) ParsedContent
ParseContent 解析规则的 Content 字段.
示例:
- "" → {Type: ContentNone}
- "prefix:npm" → {Type: ContentPrefix, Value: "npm"}
- "/src/**" → {Type: ContentPath, Value: "/src/**"}
- "domain:example.com" → {Type: ContentDomain, Value: "example.com"}
type PermissionClass ¶
type PermissionClass = string
PermissionClass 是工具的权限检查类型. 工具在 Metadata.PermissionClass 中声明,权限引擎据此分派检查逻辑.
升华改进(ELEVATED): P0-2 修复--第三方工具通过 RegisterToolClass 声明权限类型, 所有工具必须在 Metadata.PermissionClass 中声明类型,注册时自动写入 toolClassRegistry. 替代方案:修改 CheckToolPermission 签名传入 ToolRegistry(破坏所有调用点).
const ( // PermClassBash 命令执行类工具(Bash 语义:子命令分割,前缀匹配,危险命令检测). PermClassBash PermissionClass = "bash" // PermClassFile 文件操作类工具(路径权限检查,危险路径检测). PermClassFile PermissionClass = "file" // PermClassWebFetch Web 请求类工具(域名权限检查). PermClassWebFetch PermissionClass = "webfetch" // PermClassGeneric 通用工具(只读自动放行,其余默认 Ask). PermClassGeneric PermissionClass = "generic" // PermClassReadOnly 只读通用工具(等同 PermClassGeneric 但永远放行). // 用于明确声明工具无副作用,无需用户确认. PermClassReadOnly PermissionClass = "readonly" )
type PermissionDedup ¶
type PermissionDedup struct {
// contains filtered or unexported fields
}
PermissionDedup 防止同一个 tool_use_id 的权限响应被重复处理.
精妙之处(CLEVER): 用 FIFO 而不是 LRU-- tool_use_id 是一次性的,不会被"再次访问",FIFO 就够了. 替代方案:LRU(更通用但多余,tool_use_id 不存在重复访问模式).
func NewPermissionDedup ¶
func NewPermissionDedup(maxSize int) *PermissionDedup
NewPermissionDedup 创建去重器.
maxSize 指定最大容量,传入 0 使用默认值(1000 条).
func (*PermissionDedup) IsResolved ¶
func (d *PermissionDedup) IsResolved(toolUseID string) bool
IsResolved 检查指定的 tool_use_id 是否已被处理.
func (*PermissionDedup) MarkResolved ¶
func (d *PermissionDedup) MarkResolved(toolUseID string)
MarkResolved 标记指定的 tool_use_id 为已处理.
如果已标记过,不会重复添加. 如果超过 maxSize,按 FIFO 淘汰最早的条目.
type Request ¶
type Request struct {
ToolName string // 工具名称
ToolID string // 工具调用 ID
Input map[string]any // 工具输入参数
Message string // 人类可读描述
}
Request 是权限检查请求.
func (*Request) MarshalJSON ¶
MarshalJSON 支持序列化(用于 SDK 协议).
type Response ¶
type Response struct {
Decision Decision // 决策结果
Reason string // 决策原因(用于日志和调试)
RiskLevel RiskLevel // 操作风险等级(Ask 决策时填充)
SuggestedRules []SuggestedRule // 建议的永久规则(Ask 决策时填充)
// UpdatedInput, when non-nil on DecisionAllow, replaces the caller-supplied
// tool input. Lets consumer-layer permission handlers sanitize/transform
// tool arguments (e.g. rewrite Bash commands, restrict file paths, redact
// secrets) before execution. Currently consumed by SubAgent.runLoop in the
// Team permission-bubble path; engine main-thread tool exec ignores it (a
// separate wiring item).
//
// UpdatedInput 非 nil 且 Decision=Allow 时, 替换调用方传入的 tool input.
// 让消费层 permission handler 在执行前 sanitize/transform 工具参数 (改写 Bash
// 命令, 限制文件路径, 脱敏 secret 等). 目前只有 SubAgent.runLoop 在 Team 权限
// 冒泡路径消费; engine 主线程工具执行忽略 (独立 wire 项).
UpdatedInput map[string]any
}
Response 是权限决策响应.
func CheckPathPermission ¶
CheckPathPermission 检查文件路径是否被规则允许.
遍历所有规则,找到匹配的最高优先级规则. 如果没有匹配的规则,返回 nil(让调用者决定默认行为).
func CheckToolPermission ¶
CheckToolPermission 是完整的权限检查入口.
综合以下检查维度:
- 模式检查(bypass / plan 快速路径)
- 规则匹配(按优先级从低到高)
- 工具特定检查: - Bash:分割复合命令,逐个检查;检测危险命令 - 文件操作:检查路径权限和危险路径 - WebFetch:检查域名权限
- 危险检测(在规则匹配之后的额外安全层)
返回值说明:
- DecisionAllow: 允许执行
- DecisionDeny: 拒绝执行
- DecisionAsk: 需要询问用户确认
type Rule ¶
type Rule struct {
Source RuleSource // 规则来源
Behavior Decision // allow / deny / ask
ToolName string // 工具名称(支持通配符)
Content string // 规则内容(路径,前缀,域名等)
}
Rule 是一条权限规则. 对应原项目中 PermissionRule 类型.
func LoadRulesFromSettings ¶
func LoadRulesFromSettings(allowed []string, denied []string, source RuleSource) []Rule
LoadRulesFromSettings 从权限设置中加载规则列表.
AllowedTools 中的条目解析为 allow 规则, DeniedTools 中的条目解析为 deny 规则. source 参数指定规则的来源优先级.
func ParseRule ¶
func ParseRule(ruleStr string, source RuleSource, behavior Decision) Rule
ParseRule 解析规则字符串为 Rule 结构体.
规则格式:ToolName 或 ToolName(content)
示例:
- "Bash" → Rule{ToolName: "Bash", Content: ""}
- "Bash(prefix:npm)" → Rule{ToolName: "Bash", Content: "prefix:npm"}
- "Edit(/src/**)" → Rule{ToolName: "Edit", Content: "/src/**"}
- "*" → Rule{ToolName: "*", Content: ""}
type RuleApplier ¶
type RuleApplier struct {
// contains filtered or unexported fields
}
RuleApplier 将权限学习的建议转化为实际生效的 session 规则.
升华改进(ELEVATED): 不缓存决策结果,而是将重复决策升级为规则. 规则比缓存更好:Bash(prefix:npm) 覆盖所有 npm 命令, 而缓存只记住 "npm install" 这一条. 替代方案:命令指纹缓存(hash(tool_name + input) → 决策)-- 更简单但有安全风险:同一个 rm -rf build/ 在不同 cwd 下含义不同.
func NewRuleApplier ¶
func NewRuleApplier(engine Checker, learner *LearningTracker, maxRules int) *RuleApplier
NewRuleApplier 创建规则应用管道.
maxRules 指定 session 规则的最大条数,传入 0 使用默认值(100 条).
func (*RuleApplier) ApplyRule ¶
func (a *RuleApplier) ApplyRule(rule Rule) bool
ApplyRule 手动应用一条 session 规则.
如果规则已经应用过(按序列化字符串去重),则返回 false. 如果达到 maxSessionRules 上限,也返回 false.
func (*RuleApplier) ApplySuggestions ¶
func (a *RuleApplier) ApplySuggestions() []Rule
ApplySuggestions 检查学习追踪器的建议,自动应用为 session 规则.
返回本次新应用的规则列表.已经应用过的建议会被跳过. 达到 maxSessionRules 上限后停止应用新规则.
func (*RuleApplier) Reset ¶
func (a *RuleApplier) Reset()
Reset 清除所有 session 规则的追踪状态. 注意:已添加到引擎的规则不会被移除(引擎没有 RemoveRule 接口). 通常在新会话开始时,引擎也会被重新创建,所以这不是问题.
func (*RuleApplier) SessionRuleCount ¶
func (a *RuleApplier) SessionRuleCount() int
SessionRuleCount 返回当前 session 规则的数量.
type RuleDenyEngine ¶
type RuleDenyEngine struct {
// contains filtered or unexported fields
}
RuleDenyEngine 规则 deny 引擎. 只做一件事:检查工具调用是否匹配已知的危险模式.
func NewRuleDenyEngine ¶
func NewRuleDenyEngine(rules []DenyRule) *RuleDenyEngine
NewRuleDenyEngine 创建规则 deny 引擎. 如果 rules 为 nil,使用 BuiltinDenyRules.
func (*RuleDenyEngine) AddRule ¶
func (e *RuleDenyEngine) AddRule(rule DenyRule)
AddRule 向引擎追加一条 deny 规则.
type RuleSource ¶
type RuleSource string
RuleSource 是规则的来源. 决定优先级:后面的覆盖前面的.
const ( SourceUser RuleSource = "user" // ~/.flyto/settings.json SourceProject RuleSource = "project" // .flyto/settings.json SourceLocal RuleSource = "local" // .flyto/settings.local.json SourceFlag RuleSource = "flag" // --settings CLI 参数 SourcePolicy RuleSource = "policy" // 企业管理设置 SourceCLI RuleSource = "cli" // 命令行参数 SourceSession RuleSource = "session" // 会话临时规则 )
type SecurityClassifier ¶
type SecurityClassifier interface {
Classify(ctx context.Context, req *ClassifyRequest) (*ClassifyResult, error)
}
SecurityClassifier 安全分类器接口 - 模型无关. 任何实现此接口的分类器都可以被 HybridClassifier 使用.
Shape: pull. Consumer synchronously calls Classify(ctx, req) and receives a *ClassifyResult; the 6 fields (Decision / Reason / Thinking / Stage / Usage / DurationMs) are the pull payload.
形态: 调取 (pull). 消费者同步调 Classify(ctx, req) 拿 *ClassifyResult; 6 字段 (Decision / Reason / Thinking / Stage / Usage / DurationMs) 即 pull payload.
func NewAnthropicClassifier ¶
func NewAnthropicClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier
NewAnthropicClassifier 创建 Anthropic 特化的分类器.
Anthropic 实现特点:
- prompt caching(分类器系统提示跨调用复用)
- 标准 AIClassifier(XML 输出天然适合 Anthropic 模型)
- cache_control: ephemeral 标记可在上层配置
func NewClassifierForProvider ¶
func NewClassifierForProvider(providerName string, provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier
NewClassifierForProvider 根据提供商名称创建分类器. 如果提供商未注册,使用 generic 兜底.并发安全(读锁).
func NewClassifierFromRegistry ¶
func NewClassifierFromRegistry(registry *config.ModelRegistry, provider flyto.ModelProvider) SecurityClassifier
NewClassifierFromRegistry 使用 ModelRegistry 和注入的 ModelProvider 创建分类器. 从注册表中获取 RoleFast 和 RoleMain 的模型 ID,通过模型 ID 自动检测提供商类型.
升华改进(ELEVATED): 不硬编码模型 ID,完全通过 ModelRegistry 解耦. 用户切换模型只需修改角色映射,分类器自动适配. 此前版本通过 legacyClientProvider 桥接 api.Client-- 那个路径将 Anthropic HTTP 客户端泄漏给所有分类器,OpenAI/Gemini 无法使用. 现在直接接受 flyto.ModelProvider 接口,provider 由调用方注入,分类器完全 Provider 无关. 替代方案(原方案): apiKey+baseURL 参数 + legacyClientProvider 桥接-- 调用方仍需提供 Anthropic 凭据,即使实际 provider 是 OpenAI.
func NewGenericClassifier ¶
func NewGenericClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier
NewGenericClassifier 创建通用兜底分类器.
Generic 实现(兜底):
- 纯文本 prompt
- regex 解析 ALLOW/BLOCK
- 不依赖任何提供商特有功能
func NewGoogleClassifier ¶
func NewGoogleClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier
NewGoogleClassifier 创建 Google 特化的分类器.
⚠️ 占位实现(PLACEHOLDER):当前与 NewAnthropicClassifier 完全相同.
Google AI(Gemini)的 API 协议与 Anthropic 差异更大(REST-only,safety_settings 参数, grounding 功能,不同的 SSE 格式),通过 api.Client 调用时会产生请求/解析错误. 请勿在生产环境中将 provider 设为 "google",直到专用适配层完成.
技术债记录(P1-6):未来需实现 GoogleClassifier,差异点:
- Endpoint: generativelanguage.googleapis.com
- Auth: ?key=<API_KEY> 或 Bearer token
- 请求格式: generateContent(与 /v1/messages 完全不同)
- 响应格式: candidates[].content.parts[].text
- 推荐利用 grounding 特性增强分类准确性
历史包袱(LEGACY): 与 NewOpenAIClassifier 同样的占位问题.
func NewOpenAIClassifier ¶
func NewOpenAIClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier
NewOpenAIClassifier 创建 OpenAI 特化的分类器.
⚠️ 占位实现(PLACEHOLDER):当前与 NewAnthropicClassifier 完全相同.
OpenAI 的 API 协议(JSON body schema,响应格式,SSE 事件名)与 Anthropic 不兼容, 通过同一个 api.Client 调用 OpenAI 模型时 SSE 解析会失败或产生错误结果. 调用此工厂的代码将获得一个"表面可用但实际行为不正确"的分类器-- 请勿在生产环境中将 provider 设为 "openai",直到专用适配层完成.
技术债记录(P1-6):未来需实现 OpenAIClassifier,差异点:
- API endpoint: /v1/chat/completions(非 /v1/messages)
- Auth header: Authorization: Bearer(非 x-api-key)
- 响应 JSON 结构: choices[].message.content(非 content[].text)
- SSE event 格式: data: {delta: {content: ...}}(非 content_block_delta)
- 推荐用 JSON mode(response_format: {type: "json_object"})取代 XML 解析
历史包袱(LEGACY): 占位实现返回了与 Anthropic 相同的 AIClassifier, 使调用方误以为切换 provider 字段即可无缝切换供应商.
type SedCheckResult ¶
type SedCheckResult struct {
Safe bool // 是否安全
Reason string // 原因说明
Pattern string // 模式类型:"print" / "substitution" / "dangerous" / "unknown"
}
SedCheckResult 是 sed 命令安全检查的结果.
func CheckSedCommand ¶
func CheckSedCommand(command string, allowFileWrites bool) SedCheckResult
CheckSedCommand 检查 sed 命令是否安全.
检查流程:
- 提取 sed 表达式(-e,--expression=,内联表达式)
- 检查每个表达式是否包含危险操作
- 如果全部安全,判断是打印模式还是替换模式
allowFileWrites: 是否允许文件写入(w 标志).一般为 false.
type SuggestedRule ¶
type SuggestedRule struct {
// RuleString 是规则字符串,如 "Bash(prefix:npm)"
RuleString string
// Description 是规则的人类可读描述
Description string
}
SuggestedRule 是一条建议的永久权限规则. 当用户反复批准同类操作时,可以建议添加永久规则以减少打扰.
func SuggestRules ¶
func SuggestRules(toolName string, input map[string]any) []SuggestedRule
SuggestRules 为权限请求建议可添加的永久规则.
返回的建议规则可以告知用户:"如果你信任这类操作,可以添加规则以自动放行."
type TranscriptEntry ¶
TranscriptEntry 对话历史的投影条目.
func BuildTranscript ¶
func BuildTranscript(messages []query.Message, maxEntries int) []TranscriptEntry
BuildTranscript 从对话消息中构建分类器的对话历史投影.
投影规则:
- 用户消息:保留文本内容(截断到 200 字符)
- 助手消息:只保留 tool_use 调用(压缩为一行)
- 系统消息:跳过
- 最多保留最近 maxEntries 条(0 表示默认 20 条)
升华改进(ELEVATED): 排除助手的文本回复,只保留工具调用. 这是一个安全决策:如果保留助手文本,攻击者可以通过 prompt injection 让助手在文本回复中写入"用户已授权此操作"来影响分类器. 只保留 tool_use 记录是客观事实,无法被注入操纵. 替代方案:保留完整对话(包含助手文本回复,可能被 prompt injection 利用).