package permission // 文件系统权限检查. // // 提供文件路径的权限检查功能,包括: // - 路径 glob 匹配 // - 危险路径检测 // - 受保护目录检查 import ( "path/filepath" "strings" ) // protectedDirs 是受保护的目录列表. // 对这些目录下文件的操作需要额外权限. var protectedDirs = []string{ ".git", ".ssh", ".flyto", ".gnupg", ".config", "node_modules", } // dangerousPaths 是危险的文件路径模式. // 写入这些路径可能影响系统安全或行为. // // 设计边界(2026-04-15 dogfood 审计归档): // 本列表**故意不追求穷尽秘密文件枚举**, 只收录"写入会影响系统/shell/git 行为"或 // "路径本身是广泛共识的高危路径"(如 /etc/passwd, ~/.ssh/). 以下类型路径**不在**列表中, // 是刻意的 gap, 不是遗漏: // - ~/.aws/credentials, ~/.aws/config (AWS access key) // - ~/.kube/config (Kubernetes cluster admin token) // - ~/.docker/config.json (Docker registry auth) // - ~/.netrc (HTTP basic auth) // - ~/.pypirc, ~/.cargo/credentials, ~/.huggingface/token (包管理器 token) // - 以及任何未来新工具产生的秘密落地文件 // // 不扩展的理由(对齐 L513 审计方法论): // 1. 黑名单永远枚举不全 — L513 删 Bash filterSensitiveEnv 就是因为 8 条黑名单 // 漏 20+ 条真实秘密 env, 同类错误不应在路径层重演. // 2. 每加一条黑名单都在积累维护债 — 新工具产生新秘密路径的速度 > 维护者追赶速度. // 3. Read 工具 auto-approve 的前提是"本地 CLI 不做进程沙盒", 这条决策在 // memory project_sandbox_local_vs_cloud.md 归档, 对齐 Claude Code/Cursor/Aider 业内默认. // // 正确的最终解法不在 core/ 而在 platform/ 层容器沙盒(云端 SaaS 路线): // - 本地 CLI: 用户 responsible for 不把秘密放在 Agent reachable filesystem, // 或使用 engine.SetSecret / -secret-env 显式登记需保护的值 (L513 产品端闭环) // - 云端 SaaS: platform/common/internal/sandbox 在容器里运行 Agent, 宿主文件系统 // 天然不可达, 从根源消除这类 gap. 见 platform/common/internal/sandbox/ (目前占位) // // 何时可以添加新条目: 仅当该路径的"写入"是"不可逆地破坏系统"级别 // (如 /etc/sudoers), 而非"读取泄漏秘密". 后者一律推给 SecretStore / 沙盒. var dangerousPaths = []string{ // Shell 配置 "/.bashrc", "/.bash_profile", "/.bash_login", "/.bash_logout", "/.zshrc", "/.zprofile", "/.zshenv", "/.profile", // Git 配置 "/.gitconfig", "/.git/config", "/.git/hooks/", // SSH "/.ssh/", // 系统配置 "/etc/passwd", "/etc/shadow", "/etc/sudoers", "/etc/hosts", // Flyto 配置 "/.flyto/settings.json", "/.flyto/settings.local.json", "/.mcp.json", // 编辑器配置 "/settings.json", "/.vscode/settings.json", "/.idea/", } // CheckPathPermission 检查文件路径是否被规则允许. // // 遍历所有规则,找到匹配的最高优先级规则. // 如果没有匹配的规则,返回 nil(让调用者决定默认行为). func CheckPathPermission(path string, rules []Rule) *Response { path = normalizePath(path) var bestMatch *Rule bestPriority := -1 for i := range rules { rule := &rules[i] // 解析规则内容 parsed := ParseContent(rule.Content) // 只匹配路径类型的规则 if parsed.Type != ContentPath && parsed.Type != ContentNone { continue } // ContentNone 意味着匹配所有路径 if parsed.Type == ContentPath { if !GlobMatch(parsed.Value, path) { continue } } priority := SourcePriority(rule.Source) if priority > bestPriority { bestMatch = rule bestPriority = priority } } if bestMatch == nil { return nil } return &Response{ Decision: bestMatch.Behavior, Reason: "path rule from " + string(bestMatch.Source) + ": " + SerializeRule(*bestMatch), } } // IsDangerousPath 检查路径是否为危险路径. // // 危险路径包括: // - Shell 配置文件(.bashrc 等) // - Git hooks // - SSH 密钥和配置 // - 系统关键文件 func IsDangerousPath(path string) bool { path = normalizePath(path) // 检查路径是否匹配危险路径模式 for _, pattern := range dangerousPaths { if strings.HasSuffix(pattern, "/") { // 目录前缀匹配 if strings.Contains(path, pattern) { return true } } else { // 文件名匹配(检查路径是否以该文件名结尾,或包含该路径段) if strings.HasSuffix(path, pattern) || strings.Contains(path, pattern+"/") { return true } // 也检查 basename base := filepath.Base(path) patternBase := filepath.Base(pattern) if base == patternBase { return true } } } // 检查是否在受保护目录中 if IsProtectedDir(path) { return true } return false } // IsProtectedDir 检查路径是否在受保护的目录中. func IsProtectedDir(path string) bool { path = normalizePath(path) parts := strings.Split(path, "/") for _, part := range parts { for _, dir := range protectedDirs { if part == dir { return true } } } return false } // 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 GlobMatch(pattern, path string) bool { pattern = normalizePath(pattern) path = normalizePath(path) // 处理 ** 通配符:将 ** 替换为特殊标记后分段匹配 if strings.Contains(pattern, "**") { return globMatchDoublestar(pattern, path) } // 不含 ** 的情况,使用标准 filepath.Match matched, err := filepath.Match(pattern, path) if err != nil { return false } return matched } // globMatchDoublestar 处理包含 ** 的 glob 模式匹配. func globMatchDoublestar(pattern, path string) bool { // 将模式按 ** 分割 parts := strings.Split(pattern, "**") if len(parts) == 1 { // 没有 **,使用普通匹配 matched, _ := filepath.Match(pattern, path) return matched } // 第一部分必须匹配路径的开头 if parts[0] != "" { prefix := strings.TrimSuffix(parts[0], "/") if !strings.HasPrefix(path, prefix) { return false } path = path[len(prefix):] } // 最后一部分必须匹配路径的结尾 lastPart := parts[len(parts)-1] if lastPart != "" && lastPart != "/" { suffix := strings.TrimPrefix(lastPart, "/") if suffix != "" { // 使用 filepath.Match 匹配尾部 base := filepath.Base(path) matched, _ := filepath.Match(suffix, base) if !matched { // 也尝试完整路径后缀匹配 if !strings.HasSuffix(path, "/"+suffix) && path != suffix { return false } } } } // 中间部分:** 可以匹配任意层级 // 只要开头和结尾都匹配,中间的 ** 自动成立 return true } // normalizePath 规范化路径. // 清理多余的斜杠,统一使用 /. func normalizePath(path string) string { // 统一路径分隔符 path = filepath.ToSlash(path) // 清理路径 path = filepath.Clean(path) path = filepath.ToSlash(path) return path } // ExtractFilePath 从工具输入参数中提取文件路径. // // 支持的参数名:file_path, path, filepath, filename func ExtractFilePath(input map[string]any) string { // 按优先级检查不同的参数名 keys := []string{"file_path", "path", "filepath", "filename"} for _, key := range keys { if v, ok := input[key]; ok { if s, ok := v.(string); ok && s != "" { return s } } } return "" } // ExtractURL 从工具输入参数中提取 URL. func ExtractURL(input map[string]any) string { if v, ok := input["url"]; ok { if s, ok := v.(string); ok { return s } } return "" } // ExtractDomainFromURL 从 URL 中提取域名. // // 示例: // - "https://example.com/path" → "example.com" // - "http://api.github.com:443/v1" → "api.github.com" func ExtractDomainFromURL(url string) string { // 去除协议前缀 if idx := strings.Index(url, "://"); idx >= 0 { url = url[idx+3:] } // 去除路径 if idx := strings.IndexByte(url, '/'); idx >= 0 { url = url[:idx] } // 去除端口 if idx := strings.LastIndexByte(url, ':'); idx >= 0 { url = url[:idx] } // 去除认证信息 if idx := strings.IndexByte(url, '@'); idx >= 0 { url = url[idx+1:] } return strings.ToLower(url) } // 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 MatchDomain(ruleDomain, actualDomain string) bool { ruleDomain = strings.ToLower(ruleDomain) actualDomain = strings.ToLower(actualDomain) if ruleDomain == actualDomain { return true } // 子域名匹配 if strings.HasSuffix(actualDomain, "."+ruleDomain) { return true } return false }