package restrict import "strings" // 本文件是 design/11 §4 敏感清单里**按文件落点**分类的结构化判据表:同一个 // `file` 调用,落点不同则操作类别不同——写配置件是「改配置与权限」、写数据 // 存储件是「写数据」、写别处是「修改文件」;读密钥件是「访问密钥与隐私」, // 读别处仍可自主(design/11 §4「非敏感(读文件 / 检索 / 查记忆)可自主」)。 // // 判据只有**路径字符串**这一个输入(工具名 + 参数),不含任何模型文本 // (design/11 §0 红线:身份与权限永不来自提示词)。 // 配置与权限的落点(改配置与权限)。这些文件承载模型/渠道/工具策略,改动它们 // 等于改动实例的行为与权限边界,因此比普通「修改文件」更敏感: // - `.env`(含 `.env.*`):实例级开关与凭据,design/04 §4 的「.env.example 同步」对象; // - `models.json` / `channels.json`:模型路由与渠道凭据(design/04 §7、design/08); // - `mcp.json` / `plugins.json` / `policy.json` / `permissions.json` / `acl.json`: // MCP server、插件与工具策略(design/11 §3 策略落盘的相邻载体)。 var configArtifactBases = map[string]bool{ ".env": true, "models.json": true, "channels.json": true, "mcp.json": true, "plugins.json": true, "policy.json": true, "permissions.json": true, "acl.json": true, } // configDirSegments 是「目录即配置区」的判据:这些目录下的 `.yaml/.yml/.toml/ // .ini/.conf` 都是实例配置(角色定义、技能定义、用户档),改动同样归入 // 改配置与权限,而不是普通改文件。 var configDirSegments = map[string]bool{ "configs": true, "config": true, "personas": true, "users": true, "skills": true, } // configFileExts 是配置区里作为配置对待的扩展名。 var configFileExts = map[string]bool{ ".yaml": true, ".yml": true, ".toml": true, ".ini": true, ".conf": true, } // 密钥与隐私的落点(访问密钥与隐私)。这些文件里的内容**不许进日志、不许进 // 模型上下文**(design/04 §4「models.json/channels.json 含 token/key(敏感)→ // 不打印、不进日志」),所以连「读」都算敏感: // - `.env`(含 `.env.*`):凭据; // - `models.json` / `channels.json`:供应商与渠道 token/key; // - 密钥材料:`*.pem` / `*.key` / `*.p12` / `*.pfx` / `*.jks` / `*.keystore` / // `*.ppk`,以及 SSH 私钥 `id_rsa*` / `id_dsa*` / `id_ecdsa*` / `id_ed25519*`; // - 凭据库:`.netrc` / `.npmrc` / `.git-credentials` / `credentials*.json` / // `secrets*.json`,以及 `secrets` / `credentials` 目录下的任何文件。 var secretArtifactBases = map[string]bool{ ".env": true, "models.json": true, "channels.json": true, ".netrc": true, ".npmrc": true, ".git-credentials": true, "credentials": true, "credentials.json": true, "credential.json": true, "secrets.json": true, "secret.json": true, "passwd": true, "shadow": true, } // secretArtifactPrefixes 按前缀命中的密钥件(如 `id_rsa` / `id_ed25519.pub` 之外 // 的私钥族)——`id_rsa.pub` 是公钥,不含秘密,故单独排除。 var secretArtifactPrefixes = []string{"id_rsa", "id_dsa", "id_ecdsa", "id_ed25519"} // secretArtifactExts 是密钥材料的扩展名。 var secretArtifactExts = map[string]bool{ ".pem": true, ".key": true, ".p12": true, ".pfx": true, ".jks": true, ".keystore": true, ".ppk": true, } // secretDirSegments 是「目录本身即密钥区」的判据。 var secretDirSegments = map[string]bool{"secrets": true, "credentials": true} // dataStoreSuffixes 是数据存储的落点(写=写数据,读=访问密钥与隐私:库里是 // 主人的会话、记忆与审计数据,design/16 §5「敏感:本地 SQLite」)。SQLite 的 // WAL/SHM 同伴文件同属该存储。 var dataStoreSuffixes = []string{".db", ".sqlite", ".sqlite3", ".db-wal", ".db-shm", ".sqlite-wal", ".sqlite-shm"} // isConfigArtifact 报告路径是否落在配置与权限的载体上(design/11 §4)。 func isConfigArtifact(p string) bool { segs, base := splitPath(p) if base == "" { return false } if base == ".env" || strings.HasPrefix(base, ".env.") || configArtifactBases[base] { return true } return hasSegment(segs, configDirSegments) && extOf(base) != "" && configFileExts[extOf(base)] } // isSecretArtifact 报告路径是否落在密钥与隐私的载体上(design/11 §4)。 func isSecretArtifact(p string) bool { segs, base := splitPath(p) if base == "" { return false } if base == ".env" || strings.HasPrefix(base, ".env.") || secretArtifactBases[base] { return true } if secretArtifactExts[extOf(base)] { return true } for _, prefix := range secretArtifactPrefixes { if strings.HasPrefix(base, prefix) && !strings.HasSuffix(base, ".pub") { return true } } return hasSegment(segs, secretDirSegments) } // isDataStoreArtifact 报告路径是否是数据存储文件(SQLite 及同伴文件)。 func isDataStoreArtifact(p string) bool { _, base := splitPath(p) if base == "" { return false } for _, suffix := range dataStoreSuffixes { if strings.HasSuffix(base, suffix) { return true } } return false } // splitPath 把路径规范化成小写分段与末段:同时接受 Windows 反斜杠与 POSIX // 斜杠,丢弃空段(因此 `data//models.json` 与 `/data/models.json` 判成同一类)。 // 这里刻意只做**词法**规范化,与 file 工具的沙箱收敛/文件级互斥同口径:不做 // 符号链接解析,也不假装能识别 8.3 短名等物理同一写法。 func splitPath(p string) ([]string, string) { s := strings.ToLower(strings.TrimSpace(p)) s = strings.ReplaceAll(s, "\\", "/") segs := make([]string, 0, 4) for _, seg := range strings.Split(s, "/") { if seg != "" { segs = append(segs, seg) } } if len(segs) == 0 { return nil, "" } return segs, segs[len(segs)-1] } // hasSegment 报告分段里是否出现集合中的任一名字(目录判据用)。 func hasSegment(segs []string, set map[string]bool) bool { for _, seg := range segs { if set[seg] { return true } } return false } // extOf 返回小写扩展名(含点);无扩展名返回 ""。 func extOf(base string) string { i := strings.LastIndex(base, ".") if i <= 0 { return "" } return base[i:] }