API Key 是用户最敏感的资产。一旦泄露,损失可能是真实的。
桌面 Agent 安全的特殊性
讨论安全之前,先要认清一个根本事实:桌面 Agent 的安全威胁模型与 Web Agent 截然不同。
Web Agent 运行在云端沙箱里,能做的事情受到严格约束。它没有本地文件系统访问权,没有 shell 执行能力,输入和输出都经过网关层过滤。即使 Agent 被 Prompt 注入攻击诱导做了危险操作,损害范围通常限制在会话内部。
桌面 Agent 不同。Alice 运行在用户的 macOS 上,拥有 Electron 进程的全部权限:可以读写文件系统任意位置、可以执行任意 shell 命令、可以发起网络请求到任意地址、可以访问剪贴板、可以操控其他应用。换句话说,Alice 在能力层面等价于一个拥有用户完整权限的程序员。
这意味着安全威胁不再是学术假设。一段被模型误解的指令、一次被注入的 Prompt,都可能转化为真实的文件删除、密钥泄露或数据外传。
Cursor 和 Claude Code 面临同样的问题。Cursor 作为 IDE 插件运行,天然拥有代码仓库的完整读写权限。Claude Code 作为终端 Agent,shell 权限甚至比 IDE 更不受限。三者的安全设计思路各有侧重,但都必须回答同一个核心问题:如何在赋予 Agent 足够能力完成工作的同时,防止它造成不可逆的损害。
安全设计的三个层次
Alice 的安全体系分为三个互相独立的层次,每层解决不同类别的问题:
静态安全层处理数据保护。API Key 加密存储、敏感字段隔离、备份文件安全。即使设备被盗或文件被拷贝,攻击者也无法获取明文密钥。
动态安全层处理行为控制。权限分级、操作确认、工具过滤。在 Agent 执行过程中实时判断哪些操作允许自动执行、哪些需要人工确认、哪些直接拒绝。
渠道安全层处理外部输入可信度。微信消息、Telegram 消息、API 调用等外部渠道的指令,需要额外的安全审查。因为外部渠道是 Prompt 注入攻击最容易进入的入口。
三层各自独立,互不依赖。即使某一层完全失效,其他层仍然提供保护。
主密钥体系的设计取舍
为什么选择零知识加密
Alice 需要持久化存储一系列敏感数据:多个 LLM Provider 的 API Keys、聊天机器人的 Bot Token(微信、Telegram)、对话历史(可能包含隐私信息)、用户画像和长期记忆。
如果这些数据以明文存在 SQLite 里,任何能访问本地文件系统的程序都能读取。macOS 的沙箱保护对 Electron 应用并不严格,同一用户下的其他进程、恶意软件、甚至一个不小心授权了全盘访问的备份工具,都可能接触到这些文件。
加密方案有三种主流选择:
方案一:系统钥匙串(Keychain)。macOS 和 Windows 都提供了系统级的密钥管理服务。优点是用户无需设置额外密码,系统已经通过开机密码保护了钥匙串。缺点是跨平台行为不一致,macOS 的 Keychain、Windows 的 Credential Manager、Linux 的 Secret Service 各有差异;更关键的是,钥匙串通常用于存储少量短字符串,存几十个 API Key 没问题,存加密后的对话历史就不太合适了。此外,钥匙串的数据备份和迁移也依赖系统行为,用户换电脑时恢复流程不可控。
方案二:应用内固定密钥加密。在应用代码里嵌入一个固定密钥,用它加密所有数据。这是最简单的方案,也是安全性最差的方案。逆向 Electron 应用的 asar 包不到五分钟,固定密钥等于没有密钥。很多 Electron 应用(包括一些知名产品的早期版本)犯过这个错误。
方案三:用户密码派生密钥(零知识加密)。用户设置一个密码,密码通过专用的密钥派生函数(大量迭代次数)生成主密钥,主密钥用于对称加密。服务端(或应用本身)从不存储密码,只存储随机盐值和验证哈希。
Alice 选择了方案三。核心理由是:只有零知识加密才能保证即使应用数据文件被完整窃取,攻击者也无法解密。代价是显而易见的,也是不可回避的:
忘记密码意味着永久丢失。没有密码重置、没有后门、没有恢复手段。密文在没有原始密码的情况下在数学上不可逆。这是一个让很多产品经理不舒服的设计决策,但在安全领域它是正确的。任何提供密码重置能力的加密方案,都意味着存在一条不需要用户密码就能解密的路径,而这条路径就是攻击面。
1Password、Bitwarden 等密码管理器采用完全相同的策略。用户注册时会收到一段紧急恢复密钥,如果主密码和恢复密钥都丢失,账户数据就永远无法恢复。Alice 目前没有恢复密钥机制,但这是一个值得加入的安全网。
密钥派生的参数选择
迭代次数是一个经过权衡的数值。苹果在 iOS 的数据保护中使用了类似量级的迭代次数。每次验证密码的耗时对用户来说几乎无感知,但对暴力破解来说每秒只能尝试极少次数。
更高的迭代次数会提供更强的保护,但在老旧设备上解锁等待时间可能让用户感到明显延迟。当前配置是安全性和响应速度之间的实用平衡点。
未来如果需要升级到更抗 GPU 并行攻击的算法,加密标记的版本号可以平滑过渡。
字段级加密 vs 全库加密
加密的粒度是一个关键的架构决策。有两种主流方案:
全库加密
SQLCipher 是最成熟的 SQLite 全库加密方案。打开数据库时提供密码,整个数据库文件被透明加密,所有读写操作自动加解密。对应用代码几乎零侵入。
全库加密的优点很明显:保护范围最广,所有数据都被加密,不存在遗漏的可能;实现成本低,只需在数据库连接时提供密码。
但全库加密有几个在 Agent 场景下不可忽视的缺点:
性能开销均匀分摊。每一次数据库读写都要经过加解密,包括读取会话列表、查询消息记录、更新元数据这些高频操作。在长对话场景下(一次会话可能有数百条消息),解密开销会累积成可感知的延迟。
原生工具不可用。全库加密后,sqlite3 命令行工具、DB Browser 等常用的数据库调试工具都无法直接打开文件,开发调试的效率大幅下降。
备份和迁移复杂。备份文件也是加密的,恢复时需要正确的密码。如果用户在不同设备上使用不同的密码(虽然不应该,但实际会发生),备份恢复会变得混乱。
依赖 native 模块。SQLCipher 是 C 语言扩展,需要编译 native 模块。在 Electron 的跨平台打包流程中,native 模块是已知的稳定性隐患,不同系统版本、不同架构(x64 vs arm64)都需要单独编译和测试。
字段级加密
Alice 选择的方案是字段级加密:只加密真正敏感的字段,其余保持明文。
加密的字段:LLM 服务商的 API Key、消息渠道的 Bot Token、向量数据库里的记忆文本(可选)。
不加密的字段:对话消息内容、会话元数据(标题、时间、模型 ID)、系统配置。
这个选择背后的逻辑是风险分级。API Key 泄露会导致直接经济损失,攻击者可以用你的 Key 调用 API 产生费用,甚至用来做违规操作导致账号封禁。Bot Token 泄露可以让攻击者以你的身份发送消息。这两类数据的泄露后果是确定的、可量化的、立即发生的。
对话内容泄露的后果相对模糊。大多数用户的对话内容不包含高度敏感信息,而加密大量消息的性能代价很高。这是一个有意识的取舍,在安全和性能之间选择了务实的平衡。
字段级加密的实现有一个重要细节:加密后的值带有明确的格式标记,系统读取时检查标记决定是否需要解密。版本号预留算法升级空间,未来引入新算法时可兼容旧数据。
向量数据库的特殊处理
向量数据库里存储的是对话历史的语义化摘要,可能包含敏感信息。处理方式是:如果加密模块已就绪,写入向量库前加密文本,检索时解密后返回明文。
向量本身(embedding 数值)不加密。这是数学约束使然,加密会改变向量的数值分布,导致余弦相似度计算失效,相似度搜索将返回完全错误的结果。这意味着攻击者可以通过分析 embedding 向量推断原文的语义主题(虽然不能还原原文),这是一个已知的安全局限。
备份格式的演进
备份功能的设计演进是一个典型的工程教训。
早期方案:侧车文件
早期的备份格式很直觉:导出加密数据库的 JSON 快照,同时生成一个配套的元数据文件,里面包含密钥派生所需的盐值和验证哈希。恢复时需要两个文件同时存在。
这个方案在测试环境下工作正常,但在真实用户场景下很快暴露了问题:
用户把备份文件发邮件给自己时,只附了 JSON 文件,忘了元数据文件。用户把备份拷贝到 U 盘时,两个文件被放到了不同的文件夹。用户重命名了其中一个文件,导致配对失败。
核心问题是侧车文件违反了一个基本的可用性原则:备份应该是一个自包含的原子单元。需要多个文件配合才能恢复的备份方案,在工程上就是不完整的。
改进方案:内嵌元数据
改进后的方案把盐值、验证哈希、加密算法标识、格式版本号全部内嵌到备份文件的头部。一个文件就包含了恢复所需的所有信息(除了用户的密码)。
改进方案的另一个好处是脱机解密能力。早期方案恢复时需要 Alice 应用运行着才能提供解密服务。改进后的备份文件可以被任何正确实现了对应加密算法的工具解密,不依赖 Alice 本身。这消除了一个灾难性的风险场景:如果 Alice 应用本身出了 bug 无法启动,用户至少可以用通用工具恢复数据。
新旧格式的迁移是透明的:Alice 在读取备份文件时检查格式版本号,旧格式会自动查找同目录下的元数据文件,新格式直接从文件头读取。
解锁流程与防御性编程
启动时的解锁逻辑
Alice 启动时检查是否已设置加密。已设置则显示解锁屏幕。用户输入密码后,派生密钥并验证与存储哈希是否匹配。匹配成功后初始化加密模块,重新加载设置解密 API Keys,初始化 LLM Client,正常进入 Alice。
解锁前的 Alice 是功能残缺的。没有 API Key,无法调用任何 LLM,相当于一个空壳。用户可以看到界面,但不能进行任何 AI 对话。这是有意为之的设计:与其在未解锁状态下提供一个部分功能的假象,不如让用户明确感知到当前处于锁定状态。
防止明文覆盖加密数据
一个早期踩过的坑:用户设置了密码、API Keys 已加密,但 Alice 启动后用户还没解锁时,某个初始化代码读取了设置,然后触发了保存逻辑(可能是默认值回写),导致 API Keys 被空对象覆盖,加密数据丢失。
用户的感知是:设置了密码后 API Key 消失了。这比没有加密更糟糕,因为用户的信任被破坏了。
防御措施是:当加密模块未初始化时,设置写入接口检测到设置里有加密标记,直接拒绝写入,抛出错误。宁可报错,不可静默覆盖。
这个防御逻辑需要覆盖所有可能触发设置写入的路径:设置页面的保存按钮、初始化时的默认值回写、导入配置、恢复备份。漏掉任何一条路径都可能导致数据丢失。
权限分级:五种模式的对比
Agent 的权限控制是安全与效率之间最直接的张力点。给 Agent 的权限越多,用户操作越省力,但安全风险越大。给的权限越少,每一步都需要确认,用户体验退化成对话式的审批系统。
Alice 提供了五种权限模式,从最宽松到最严格:
Auto Mode
所有操作自动执行,不弹任何确认框。适合用户完全信任 Agent 的场景,比如在一个隔离的测试环境中运行。效率最高,风险也最高。一条被误解的指令可能直接执行破坏性操作。
Claude Code 的 Auto Mode 采用类似策略,Anthropic 官方文档明确警告这个模式应该只在沙箱环境中使用。
Smart Auto Mode
默认信任低风险操作(读取文件、搜索、网络查询),高风险操作(写入文件、执行命令、发送消息)仍需确认。风险等级由工具元数据中的风险声明字段决定。
这是 Alice 的默认模式。它的核心假设是:Agent 的大部分操作是信息收集(读),少部分是状态修改(写)。自动放行读操作可以显著减少确认次数,同时不增加实质性风险。
Ask Mode
所有工具调用都需要用户确认。这是最保守的模式,适合用户不熟悉 Agent 行为、或者处理高敏感数据时使用。缺点是用户需要频繁点击确认按钮,在多工具并发场景下尤其烦人。
Cursor 的默认行为接近 Ask Mode,每次文件修改都会在 diff 视图中展示变更并等待用户接受。这在代码编辑场景下是合理的(代码修改需要 review),但在通用 Agent 场景下过于保守。
Plan Mode
Agent 只能提出计划,不能执行任何操作。用户 review 计划后手动执行,或者切换到其他模式让 Agent 执行。
Claude Code 的 Plan Mode 是一个有趣的设计:在 Plan Mode 下,Agent 可以使用只读工具(读文件、搜索)来收集信息和制定计划,但不能使用任何写操作工具。这让 Agent 在安全的约束下充分理解任务,用户确认计划后再切换到执行模式。
Alice 参考了这个设计,但增加了一个细节:Plan Mode 下 Agent 可以调用 Think 工具来展示推理过程,让用户了解 Agent 的思考逻辑,增加透明度。
Locked Mode
完全锁定,Agent 不能调用任何工具。这个模式是为自动化测试和安全审计设计的,不用于日常场景。在 Locked Mode 下可以验证 Agent 的纯推理能力,确保它不会绕过权限系统。
五种模式的取舍总结
没有最优模式,只有最适合当前场景的模式。Alice 的策略是提供切换能力,让用户根据任务性质动态调整。处理日常编程任务时用 Smart Auto,处理生产环境部署时切换到 Ask Mode,接入外部消息渠道时用 Plan Mode。
外部渠道安全白名单
踩坑故事:微信消息触发危险命令
这个安全机制的设计源于一次真实的事故。
Alice 接入了微信作为消息渠道。用户可以通过微信给 Alice 发消息,Alice 会像在桌面端一样处理指令。某天,一个微信群里有人发了一条包含技术讨论的消息,消息内容里恰好包含了一条 shell 命令的示例。Alice 把这条消息当作用户指令,解析出了 shell 命令并试图执行。
幸好权限系统拦截了执行,但这个事件暴露了一个深层问题:外部渠道的消息和桌面端的直接对话,在信任等级上是完全不同的。
桌面端的消息来自物理键盘输入,用户坐在屏幕前,有即时的视觉反馈和取消能力。外部渠道的消息经过了网络传输,可能被中间人篡改,可能来自群聊而非私聊,可能是转发的而非原创的。
白名单机制
Alice 对外部渠道引入了多层白名单机制:
渠道级。不是所有外部渠道都允许触发工具调用。默认情况下,只有私聊渠道可以触发,群聊渠道仅回复文字不执行操作。
工具级。即使是被信任的私聊渠道,也不是所有工具都可以被触发。外部渠道只能触发安全工具(如搜索、查询),不能触发文件操作、命令执行等危险工具。
用户级。只有预先配置的特定用户(通过渠道 ID 标识)发来的消息才被处理。未授权用户的消息被忽略或返回默认回复。
这几层白名单是独立的,需要全部通过才能执行操作。任何一层拒绝,操作就被拦截。
和 Prompt 注入的关系
外部渠道的安全白名单本质上是对 Prompt 注入攻击的第一道防线。Prompt 注入的核心手法是在用户输入中嵌入看起来像系统指令的内容,诱导 LLM 执行非预期操作。外部渠道的消息天然比直接输入更容易被注入,因为攻击者可以通过群消息、转发消息、甚至修改消息的方式注入恶意内容。
白名单机制不试图判断消息内容是否是注入攻击(这在当前技术下几乎不可能可靠做到),而是从能力层面限制即使注入成功后 Agent 能做什么。这是一种纵深防御策略:承认检测不完美,转而限制损害范围。
敏感信息输出过滤
安全不仅是保护输入,也需要保护输出。
Agent 在执行工具调用时,可能读取到包含 API Key、密码、Token 等敏感信息的文件或环境变量。如果这些内容被原样传递给 LLM,再由 LLM 输出到对话界面或外部渠道,就等于泄露了敏感信息。
Alice 在两个层面进行输出过滤:
工具输出过滤。文件读取工具在返回内容前,用正则表达式扫描常见的敏感信息模式:API Key 格式(常见服务商的密钥前缀)、环境变量赋值格式、配置文件中的凭证字段。匹配到的内容被替换为屏蔽标记。
对话输出过滤。LLM 的回复在发送给用户之前,再次经过相同的过滤逻辑。这是第二道防线,防止 LLM 从其他上下文信息中推断并输出了敏感内容。
过滤的难点在于假阳性和假阴性的平衡。过于激进的过滤会把正常的技术讨论也遮蔽(比如讨论 API Key 格式的教程内容),过于宽松的过滤会漏掉非标准格式的敏感信息。目前的策略是宁可多过滤,误伤的内容用户可以在日志中查看原文。
PIN 锁屏 vs 系统级锁屏
Alice 需要在用户离开时保护应用状态。两种方案的对比:
系统级锁屏
依赖操作系统的屏幕锁定。优点是不需要额外开发,系统级的安全保障更强(保护整个桌面)。缺点是粒度太粗:锁了整个系统,不只是 Alice。用户可能只想锁 Alice 而继续使用其他应用。
应用内 PIN 锁屏
Alice 自己实现一个 PIN 输入界面,覆盖在主窗口上方。优点是粒度精确,只锁 Alice。缺点是安全性较弱:Electron 的窗口覆盖可以被系统级的窗口管理工具绕过,内存中的解锁状态可以被其他进程读取。
Alice 选择了应用内 PIN 锁屏,但做了一个设计上的区分:PIN 锁屏不等于加密解锁。PIN 只是一个界面遮罩,防止路过的人看到对话内容。主密钥仍然在内存中,加密模块仍然处于初始化状态。如果用户需要更强的保护(比如长时间离开),应该退出 Alice,下次启动时需要完整的密码解锁流程。
这个区分很重要:不能让用户误以为 PIN 锁屏提供了和密码解锁同等的安全性。PIN 锁屏的目标是便利性(快速锁定/解锁),密码锁屏的目标是安全性(密钥保护)。
和 Cursor / Claude Code 的安全策略对比
三个产品在安全层面的设计哲学有明显差异,背后反映的是不同的产品定位和威胁模型判断。
Cursor 的安全策略
Cursor 作为 IDE 插件,安全边界主要在代码编辑层面。它的核心安全机制是 diff review:每次 AI 修改代码,都以 diff 视图展示变更,用户逐行 review 后接受或拒绝。这在代码场景下很有效,因为代码修改的影响通常是可预测的、可 review 的。
Cursor 的局限在于它对文件操作以外的安全威胁缺乏覆盖。网络请求、shell 命令执行、外部服务调用等操作不在 Cursor 的安全模型范围内,因为 Cursor 的定位是代码编辑器而非通用 Agent。
Cursor 不提供 API Key 的加密存储。配置信息存储在 VS Code 的设置系统中,遵循 VS Code 的安全模型。这在 IDE 场景下是合理的(IDE 通常不存储大量敏感凭证),但不适用于 Alice 这样需要管理多个外部服务 Token 的场景。
Claude Code 的安全策略
Claude Code 的安全模型围绕权限文件展开。通过配置文件定义了哪些工具允许自动执行、哪些需要确认。这是一种声明式的权限管理,用户可以根据项目特点定制权限规则。
Claude Code 的一个独特设计是在其免确认模式的命名中包含了风险警示词。这个命名本身就是一种安全措施:通过在名称中包含警告性措辞,强制用户意识到自己在做什么。这比一个中性的命名在心理层面提供了更强的警示。
Claude Code 不提供数据加密存储。作为 CLI 工具,它不持久化存储 API Key(每次启动时从环境变量读取),因此数据保护的需求不同于 Alice 这样的 GUI 应用。
Alice 的差异化
Alice 的安全策略是三者中最重的,也是必须最重的。原因有三:Alice 是 GUI 应用,用户倾向于保持常驻运行,攻击窗口更长。Alice 持久化存储大量敏感数据(多个 Provider 的 API Key、Bot Token),保护需求最高。Alice 接入外部消息渠道(微信、Telegram),攻击面最大。
这三个因素叠加,要求 Alice 的安全体系必须在深度和广度上都超过 Cursor 和 Claude Code。代价是安全系统本身的复杂度很高,需要持续投入维护。
密码安全的工程要点
| 要点 | 设计理由 |
|---|---|
| 密码不存储,只存盐值和验证哈希 | 消除密码泄露风险 |
| 高迭代次数的密钥派生 | 限制离线暴力攻击速度 |
| 每个实例唯一的随机盐值 | 防彩虹表攻击 |
| 每次加密使用随机初始向量 | 相同明文产生不同密文 |
| 加密标记含版本号 | 支持未来算法升级 |
| 加密未就绪时拒绝写入 | 防止静默覆盖加密数据 |
| 备份文件自包含 | 单文件即可脱机恢复 |
未解决的问题与演进方向
安全是一个需要持续维护的领域。以下是当前架构中已知的局限,以及各自的演进思路:
对话内容未加密。内存转储或 SQLite 直接读取可以看到明文对话历史。这是字段级加密的有意取舍,但随着用户在对话中分享越来越多的敏感信息(代码仓库凭证、数据库连接串),这个取舍的天平正在倾斜。可能的演进是提供可选的全量加密模式,让安全需求高的用户牺牲性能换取更完整的保护。
主密钥在内存里。进程运行期间主密钥在内存里,内存转储可以提取。缓解方案包括使用操作系统提供的安全内存分配(防止页面被换出到磁盘)和定期重新派生密钥。但在 Electron 的 Node.js 运行时中,对内存管理的控制力有限,这是技术栈本身的约束。
Prompt 注入检测。当前的防御策略是限制能力范围(白名单),不是检测注入内容。学术界已经有一些 Prompt 注入检测的研究,但目前没有任何方案能提供工程上可靠的检测率。这个领域值得持续关注,但不应该把未经验证的检测方案用于生产。
审计日志。当前的日志系统记录了操作结果,但没有记录权限判断的决策过程。当用户质疑为什么某个操作被允许或被拒绝时,缺乏可追溯的审计链路。这需要在权限系统中增加结构化的决策日志。