状态:权威开发规则(authoritative) 读取时机:修改凭证或授权信息处理、文件落盘位置、用户持久数据、临时文件、 测试目录或运行时生成物之前
本文约束 Desktop、Mobile、共享 package、脚本和测试中的本地文件写入。数据库 migration
另见 database-and-migrations.md,Renderer 与 IPC 边界另见
electron-security-and-process-boundaries.md。
增量适用原则:本规则约束新增和正在修改的代码,不要求为统一目录结构专项迁移 存量数据。存量凭证或用户数据迁移必须单独设计兼容、回滚和验证方案。
- 用户凭证、OAuth 授权、API key、访问令牌、刷新令牌、私钥和 Agent 授权文件不得以 复制、链接、fixture、日志或生成物等形式进入仓库工作区或任何可能被 Git 跟踪的路径。
.gitignore只是误操作兜底,不是凭证存储方案。发现凭证曾进入 Git 历史时,应立即 停止传播并通知用户作废凭证;删除工作区文件不能撤销泄露。- 运行时需要持久化秘密时,复用现有 Main/宿主管理的 credential store 或 Electron
safeStorage边界。不要新增自定义明文凭证文件,也不要把秘密下放给 Renderer、插件 或不受信任页面。 - 可信 Node Worker 的显式例外:
node.secretBindings[].oauthSecret只能引用本插件 已声明的 OAuth key。Host 根据本次authAccount(省略时为默认账号)刷新并注入 短期 access token;不得注入 refresh token、返回 Renderer/Agent、写日志或落盘。 Worker 启动第三方 CLI 时仅用该次子进程环境传递,不修改全局环境或复用他账号配置。 这是高权限 Node 的受审查信任边界,不是系统沙箱或对恶意 Worker 的隔离保证。 实现与回归见 nodeRuntimeBroker.ts 和 nodeRuntimeBroker.test.ts。 - 插件自定义的账号昵称、展示偏好和业务配置属于插件数据,使用现有隔离
/kv, 不扩充 Host OAuth 账号模型、凭证库或专用接口。插件按账号 ID 合并这些数据用于展示 和选择账号;传给 Host 的授权身份仍是账号 ID,不能用昵称替代。 - access token 等只需短期使用的秘密优先保留在内存中。日志、错误、遥测和调试输出不得 包含凭证明文、完整鉴权头或可直接复用的授权材料。
- 测试只使用明显无效的假凭证,不读取或复制开发者真实的
HOME、Agent home、 Electron userData 或系统凭证目录。
- Desktop 在 Electron
ready之前为 Hyprland 默认选择gnome-libsecret,避免桌面 自动识别失败导致登录回调收到令牌后无法加密保存。桌面身份依次取非空的XDG_CURRENT_DESKTOP、XDG_SESSION_DESKTOP、DESKTOP_SESSION;其他桌面保持 Electron 原有选择。实现见 linuxPasswordStore.ts,回归见 linuxPasswordStore.test.ts。 - 显式
--password-store优先于此默认值。系统仍需提供可用且已解锁的 Secret Service (例如 GNOME Keyring);此修复不安装或解锁钥匙串,也不新增明文降级。现有仅限开发版的XDT_DEV_SAFE_STORAGE_BASIC=1调试行为保持不变。 - 旧版临时处理:完全退出 Cindy 后运行
cindy --password-store=gnome-libsecret。 实机验收需覆盖登录、退出应用后正常启动仍保持登录,以及cindy://回调启动路径。
- macOS 上 Electron
safeStorage的钥匙串条目名由app.name派生 (service =<app.name> Safe Storage)。当前语义(#871):packaged cn / global 与 共享 userData 的 dev 共用Cindy Safe Storage;显式隔离的 dev 沙箱 (--isolated/XDT_ISOLATED=1)在首启(profile 为空)时选定独立的CindyDev Safe Storage,并把身份写入 profile 根的keychain-identity标记文件、 跨重启粘住(见apps/desktop/src/main/devKeychainName.ts;身份不能用「目录是否 为空」做持续判据)。隔离沙箱默认目录随之升纪元为<userData>-dev2[-<名字>]:旧-dev目录属Cindy身份纪元、留给旧 checkout,同名目录被两种身份轮流打开会互毁 密文。无标记且已有数据的旧沙箱永久保持默认条目名(存量密文绑定旧 条目主密钥,零迁移只对新沙箱成立);裸设XDT_USER_DATA_DIR只是目录覆写、不表达 隔离意图(devCliFlags 契约),绝不认领CindyDev——但打开的目录已带标记时依 标记运行(观察模式:身份是 profile 的属性,不随启动旗标切换),标记不可读或内容 不可识别同样拒绝启动。空 profile 的身份在两种模式下都经标记文件原子落定 (隔离启动认领CindyDev,覆写启动认领默认身份Cindy,输家依胜者标记), 防并发启动对同一 profile 以两种身份写密文。 - 钥匙串条目名与 userData profile 的存量密文一一绑定:不得在共享既有 profile 的
进程里改
app.name——换名后新写入的密文对共用该 profile 的其它身份不可解,双向串坏。 改动条目名属存量凭证迁移,按上方增量适用原则必须单独设计兼容/回滚/验证方案。 - 同机装过 cn 与 global 双版的机器上,后启动的版本首次访问
safeStorage会触发系统 钥匙串授权弹窗,属 macOS 按预期征求同意;应引导用户点「始终允许」。点「拒绝」后 加解密降级失败,authManager 的 safeStorage helpers 会按原因落一次 warn 日志。 - 不要在启动路径主动调用
safeStorage.isEncryptionAvailable()做探测——macOS 上探测 本身可能触发钥匙串授权弹窗,把弹窗时机提前到与用户动作无关的启动期。
| 数据性质 | 正确位置 |
|---|---|
| Cindy 管理的持久数据 | Desktop 使用 app.getPath('userData'),共享 package 由宿主注入等价根目录 |
| 可丢弃的临时数据 | app.getPath('temp') 或 os.tmpdir() 下的任务专属目录 |
| 测试生成物 | os.tmpdir() 下通过 mkdtemp 创建的独立目录,并在测试结束时清理 |
| Skill 卸载清理回执 | app.getPath('userData')/skillhub/uninstall-cleanups/<token>.json,记录操作 owner、旧文件/注册/偏好身份与完成阶段;跨窗口和重启保留,当前 owner 重试完成后删除,不作为授权凭据 |
| 跨 profile 的共享 Skill 文件互斥 | app.getPath('appData')/Cindy/shared-skill-mutation-locks,仅存文件锁及未完成操作的 token/名称哈希,保证正式版/dev/isolated 共用;短期锁复用既有崩溃回收,持久屏障必须等对应清理完成后删除,读取损坏只阻止相关名称 |
| 用户明确导出的文件 | 用户选择或任务明确指定的目标路径 |
- 禁止把
process.cwd()、仓库根或源码目录作为 userData、凭证目录或临时目录的默认回退。 特别不要写process.env.TEMP ?? process.cwd()一类跨平台会落入仓库的逻辑。 - 使用
path.join、path.resolve和现有路径策略,不硬编码平台分隔符,不手拼~、%APPDATA%等平台目录。 - 共享 package 不直接依赖 Electron 来猜宿主目录;由 Desktop、Mobile 或测试显式注入路径。
- 新模块不得在 import 时创建目录、写文件或复制授权材料。文件系统副作用应在显式初始化 或用户动作中发生,便于控制路径、失败语义和清理。
- 临时文件使用唯一目录或文件名,完成、失败和取消路径都应尽力清理;需要跨重启保留的 内容不属于临时文件,应进入明确的 userData 存储与生命周期设计。
- 写入目标是否可能位于仓库、当前工作目录或被 Git 跟踪的路径?
- 是否把真实凭证带入了测试、fixture、日志、错误或 Renderer?
- 持久数据、临时数据和用户导出是否选择了正确的生命周期与目录?
- 路径回退在 Windows、macOS 和 Linux 上是否都不会落入工作区?
- package 是否由宿主注入路径,且初始化没有隐式写盘副作用?
- 失败、取消和测试结束后,临时数据是否能安全清理?
命中凭证进入仓库或不受信任边界的改动必须阻断。验证命令按
desktop-development.md 或
mobile-development.md 选择,并为路径回退、清理和秘密不外泄补
定向测试。