package builtin import ( "os" "path/filepath" "regexp" "strings" ) // isDangerousRemovalPath reports whether absolutePath is in the // system-critical directory blacklist that triggers fail-closed // rejection of rm / rmdir operations. // // Mirrors Claude Code utils/permissions/pathValidation.ts: // isDangerousRemovalPath. Blacklist: // // - "/" (root directory itself) // - direct children of root: "/etc", "/usr", "/tmp", "/var", // "/bin", "/sbin", "/lib", "/opt", "/home", "/root", "/boot", // "/sys", "/proc", "/dev" (anywhere `dirname == "/"`) // - user home directory itself ("$HOME") -- but not subdirectories // of home, which are allowed (developer projects under ~/code/x) // - Windows drive roots: "C:", "C:\\", "C:\\Windows" -- detected // via WINDOWS_DRIVE_ROOT_REGEX // - "*" or paths ending in "/*" (rm * / rm /etc/*) // // Allowed (NOT dangerous): // - cwd-relative paths ("foo/", "dist/", ".next/") // - subdirectories of system roots ("/usr/local", "/tmp/build/x") // - subdirectories of home ("/home/user/code/myproject") // // Why this design (vs hard-banning rm -rf as a flag): rm -rf in a // project's build/ output is routine; banning the flag would block // most development workflows. Banning critical PATHS catches the // catastrophic cases (deleting / or /etc) without false positives. // // isDangerousRemovalPath 检测 absolutePath 是否在系统关键目录黑名单 // 上, 命中即触发 rm / rmdir 的 fail-closed 拒绝. // // 对位 Claude Code utils/permissions/pathValidation.ts 同名函数. // 黑名单: 根 / / 直接子目录 / 用户 home 本身 / Windows 盘根 / // 通配 *. 子目录 (如 /usr/local / ~/code) 不算危险. // // 设计理由 (而非黑名单 rm -rf flag): 项目内 rm -rf build/ 是日常, // 黑名单 flag 会阻断大多开发流. 黑名单关键路径精准抓灾难场景 // (rm / 或 rm /etc) 无误伤. func isDangerousRemovalPath(absolutePath string) bool { // 统一分隔符: 反斜杠 + 多斜杠 collapse, 让 C:\\Windows 跟 C:/Windows // 走同一路径; 防止某些路径形态绕过. // // Normalize separators: collapse backslash + multiple slashes so // C:\\Windows and C:/Windows take the same path; prevents some // path-form bypasses. forwardSlash := multiSlashRegex.ReplaceAllString(absolutePath, "/") if forwardSlash == "*" || strings.HasSuffix(forwardSlash, "/*") { return true } normalized := forwardSlash if normalized != "/" { normalized = strings.TrimSuffix(normalized, "/") } if normalized == "/" { return true } if windowsDriveRootRegex.MatchString(normalized) { return true } if home := os.Getenv("HOME"); home != "" { homeNorm := strings.TrimSuffix(multiSlashRegex.ReplaceAllString(home, "/"), "/") if normalized == homeNorm { return true } } // 直接子目录: parent dir is "/" if filepath.Dir(normalized) == "/" { return true } return false } // multiSlashRegex collapses backslash and consecutive slashes into a // single forward slash. Both Windows (\\) and Unix (//) noise paths // must be normalized before blacklist comparison. // // multiSlashRegex 把反斜杠和连续斜杠 collapse 成单个正斜杠. Windows // (\\) 和 Unix (//) 噪声路径都要归一化后再做黑名单对比. var multiSlashRegex = regexp.MustCompile(`[\\/]+`) // windowsDriveRootRegex matches Windows drive roots and direct // children: "C:", "C:\\", "C:/Windows", "D:/Users", but NOT // "C:/Users/me" (which is a subdirectory and should be allowed). // // windowsDriveRootRegex 匹配 Windows 盘根及直接子目录: "C:", // "C:\\", "C:/Windows", "D:/Users", 但 不 包括 "C:/Users/me" // (子目录, 应放行). var windowsDriveRootRegex = regexp.MustCompile(`^[A-Za-z]:(/[^/]*)?$`) // extractRemovalTargets scans command for rm / rmdir invocations and // returns each non-flag token after the command name. Conservative // shell-aware split: handles spaces / quotes / && / || / ; / | as // statement boundaries; does NOT do full POSIX shell AST (use cc- // style tree-sitter for that; v1 is regex + Fields, covers 90% of // real-world LLM-emitted commands). // // extractRemovalTargets 扫 command 抓 rm / rmdir 调用, 提取命令名后 // 的非 flag token. 保守 shell-aware split: 处理空格 / 引号 / && / // || / ; / | 作为语句边界; 不做完整 POSIX shell AST (那需要 cc 风格 // 的 tree-sitter; v1 用 regex + Fields 覆盖 90% 真实 LLM 输出命令). // // 已知不抓的 case (留给 v2 / 用户报告驱动): // - eval / sh -c 包裹的 rm // - alias 改名的 rm // - 变量展开 rm $DANGEROUS_PATH // // Known un-caught cases (v2 follow-up driven by user reports): // - eval / sh -c wrapped rm // - aliased rm // - variable-expanded rm $DANGEROUS_PATH func extractRemovalTargets(command string) []string { var targets []string matches := rmInvocationRegex.FindAllStringSubmatchIndex(command, -1) for _, m := range matches { // m[0]:m[1] = full match // m[2]:m[3] = command name (rm | rmdir) // m[4]:m[5] = args portion (everything after command name until // statement boundary) argsStr := command[m[4]:m[5]] for _, tok := range strings.Fields(argsStr) { if strings.HasPrefix(tok, "-") { continue // flag } // strip surrounding quotes tok = strings.Trim(tok, `"'`) if tok == "" { continue } targets = append(targets, tok) } } return targets } // rmInvocationRegex matches rm / rmdir command invocations. Anchored // to statement boundaries (start-of-string, ;, &&, ||, |, newline) // to avoid false positives in strings like "echo 'rm -rf /'". // // Group structure: (cmd_name)(args_until_boundary) // // rmInvocationRegex 匹配 rm / rmdir 命令调用. 锚定到语句边界 // (字符串起 / ; / && / || / | / 换行) 防止字符串内的"rm -rf /" // 类似误报. // // 分组结构: (命令名)(args_直到边界) var rmInvocationRegex = regexp.MustCompile(`(?:^|[;&|\n])\s*(rm|rmdir)\b([^;&|\n]*)`) // validateDangerousRemoval scans command for rm / rmdir invocations // targeting blacklisted system paths and returns a non-empty error // message if found, empty string otherwise. cwd is used to resolve // relative paths to absolute before blacklist comparison. // // validateDangerousRemoval 扫 command 抓针对黑名单系统路径的 rm / // rmdir 调用; 命中返回非空错误消息, 否则返回空串. cwd 用于把相对 // 路径解析为绝对路径后再做黑名单对比. func validateDangerousRemoval(command, cwd string) string { for _, target := range extractRemovalTargets(command) { expanded := expandTilde(target) var abs string if filepath.IsAbs(expanded) { abs = expanded } else { abs = filepath.Join(cwd, expanded) } if isDangerousRemovalPath(abs) { return "blocked: rm/rmdir target '" + abs + "' is in the system-critical path blacklist " + "(/, /etc, /usr, /tmp, /var, /bin, $HOME root, Windows drive root, '*' wildcard). " + "This is a Flyto Agent hard contract analogous to Claude Code's pathValidation. " + "Use a more specific path inside cwd or a project subdirectory." } } return "" } // expandTilde expands a leading ~ or ~/ to the user's HOME directory. // Returns path unchanged if HOME is unset or path doesn't start with ~. // // expandTilde 把开头的 ~ 或 ~/ 展开为用户 HOME 目录. HOME 未设或 // path 不以 ~ 开头时原样返回. func expandTilde(path string) string { if !strings.HasPrefix(path, "~") { return path } home := os.Getenv("HOME") if home == "" { return path } if path == "~" { return home } if strings.HasPrefix(path, "~/") { return filepath.Join(home, path[2:]) } return path }