Tolaria 的 Tauri 资源协议用累积式 Vault 作用域修复预览 403 的取舍与实现【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolariaTolaria 是一个基于 Tauri 的 Markdown 知识库桌面应用笔记中的图片和 PDF 附件通过 Tauri 的 asset protocol 在 WebView 中渲染。ADR-0099 记录了一个典型的桌面端权限模型修正前一条决策 ADR-0074 把资源访问从“全文件系统开放”收紧为“运行时仅允许当前 Vault”但因 Tauri 的 forbid 规则不可逆切换 Vault 再切回后图片与 PDF 预览会持续返回403 Forbidden。本篇基于该决策记录及其在 src-tauri/src/asset_scope.rs 中的落地实现讲清楚 Tolaria 如何用一个“累积式运行时作用域 命令层活跃 Vault 边界”的两层模型同时保证预览可靠性与最小权限原则。背景从 ADR-0074 的收紧到 403 回归缺陷理解本决策需要先理解它修正的问题。ADR-0074 的完整路径见 docs/adr/0074-explicit-external-ai-tool-setup-and-least-privilege-desktop-scope.md其核心动作之一是Tauri 的 asset protocol 静态配置作用域置空assetProtocol: { enable: true, scope: [] }见 src-tauri/tauri.conf.json 第 50-53 行改为在 Vault 重新加载时于运行时仅向该 Vault 授予递归资源访问权限。ADR-0099 的 Context 部分指出该实现为了“只保留活跃 Vault 在作用域内”在 Vault 不再是活跃状态时会调用 Tauri 的forbid_directory。这里踩到了 Tauri 文件系统作用域的语义陷阱forbidden 路径是永久性优先规则一个路径一旦被 forbid即使之后再次 allow 也会被拒绝因此用户从 Vault A 切走再切回后A 中的合法图片与 PDF 文件会持续产生403 Forbidden响应直到应用重启。这是一个“收紧权限的尝试反而破坏了核心功能”的回归。ADR-0099 的 frontmatter 中supersedes: 0074 asset-protocol runtime scoping也表明它部分取代了 ADR-0074 中关于资源协议运行时作用域的做法——保留了“静态作用域为空、运行时按 Vault 授权”的方向但放弃了“运行时撤销”的假设。决策累积授权永不 forbidADR-0099 的决策原文可以概括为一句话Tolaria 对当前应用会话中加载过的所有 Vault 根目录累积保留 Tauri asset protocol 访问权限并在运行时永不 forbid 曾经加载过的 Vault 根。展开为四条要点sync_vault_asset_scope在规范路径canonical root和请求路径requested root缺失于运行时作用域时将其补入运行时作用域仍比全局文件系统访问窄——只有 Tolaria 实际加载过的 Vault 根会被加入命令路径读、写、外部打开、附件导入仍然在 Rust 命令层强制活跃 Vault 边界见 src-tauri/src/commands/vault/boundary.rs作用域撤销被推迟到进程退出因为 Tauri 没有暴露对目录的安全运行时 unallow 操作。换句话说权限撤销的边界从“每次 Vault 切换”改为了“整个应用进程生命周期”。实现剖析asset_scope.rs 的累积式授权核心实现集中在 src-tauri/src/asset_scope.rs总共不到 60 行结构非常清晰。会话级已授权根目录清单pub(crate) struct AllowedAssetScopeRoots(pub(crate) MutexVecPathBuf);这是一个进程级全局状态在 src-tauri/src/lib.rs 第 426 行通过.manage(...)注册进 Tauri 的 state。它记录本次会话中已经 allow 过的所有 Vault 根是“累积”语义的载体——注意它只有追加路径没有任何移除逻辑。为什么同时加入 canonical 与 requested 两个根pub(crate) fn vault_asset_scope_roots(vault_path: Path) - ResultVecPathBuf, String { let canonical_vault_path std::fs::canonicalize(vault_path).map_err(...)?; let mut roots vec![canonical_vault_path.clone()]; let requested_vault_path vault_path.to_path_buf(); if requested_vault_path ! canonical_vault_path { roots.push(requested_vault_path); } Ok(roots) }canonicalize会解析符号链接得到真实目录路径如果用户传入的是符号链接路径例如通过快捷方式或别名打开的 Vault则它与规范路径不同。两者都加入作用域可以覆盖前端以不同形式引用同一 Vault 的情况。这一点在 src-tauri/src/lib_tests.rs 第 148-159 行的vault_asset_scope_roots_include_requested_symlink_path测试中有专门验证创建一个真实目录和一个指向它的符号链接断言返回的根列表中既包含规范路径也包含符号链接路径。幂等的 allow_directory 调用pub(crate) fn sync_vault_asset_scope( app_handle: tauri::AppHandle, vault_path: Path, ) - Result(), String { use tauri::Manager; let requested_roots vault_asset_scope_roots(vault_path)?; let scope app_handle.asset_protocol_scope(); let state: tauri::State_, AllowedAssetScopeRoots app_handle.state(); let mut allowed_roots state.0.lock()...; let roots_to_allow missing_asset_scope_roots(allowed_roots, requested_roots); for root in roots_to_allow { scope.allow_directory(root, true)...; // 第二个参数 true 递归 } allowed_roots.extend(roots_to_allow); Ok(()) }关键设计有三处missing_asset_scope_roots做差集过滤只有未授权过的根才会真正调用scope.allow_directory(root, true)因此函数是幂等的可以安全地在多次 Vault 加载/重载时重复调用allow_directory的第二个参数为true即递归授权整个 Vault 目录树——这正是笔记内嵌图片、附件文件所需的最小单位差集逻辑由独立纯函数missing_asset_scope_roots实现便于单测。src-tauri/src/lib_tests.rs 第 161-173 行的missing_asset_scope_roots_keeps_previously_allowed_vaults断言了累积语义已允许/vault-a后请求/vault-b会返回待授权的[/vault-b]再次请求/vault-a则返回空——已授权的 Vault 永远不会被移出待处理列表更不会被 forbid。触发点哪些操作会扩大作用域从源码调用关系看资产作用域的扩大集中在三个入口Vault 重载。src-tauri/src/commands/vault/scan_cmds.rs 第 123 行reload_vault命令在刷新 Vault 缓存前先调用crate::sync_vault_asset_scope(app_handle, Path::new(path))保证每次重新加载一个 Vault 后其中媒体的 asset URL 必然可访问附件保存/导入类命令。src-tauri/src/commands/vault/file_cmds.rs 中的with_image_asset_scope帮助函数在附件落盘成功后同步调用sync_image_asset_scope内部按#[cfg(desktop)]门控非桌面平台直接跳过使新导入的图片立即可在编辑器中预览笔记窗口。file_cmds.rs 第 81-88 行暴露了 Tauri 命令sync_vault_asset_scope_for_window前端在 src/hooks/useNoteWindowLifecycle.ts 第 56-59 行的syncVaultAssetScope中调用它并在加载笔记内容前第 52 行await syncVaultAssetScope(vaultPath)先行完成授权再invoke(get_note_content, ...)——这样二级笔记窗口打开含图片的笔记时不会闪 403。这条链路与 ADR-0099 中sync_vault_asset_scope 在缺失时补入根目录的描述一一对应。备选方案对比为什么不选更“严格”的方案ADR-0099 列出了四个选项值得完整保留因为它们解释了取舍逻辑方案结论理由累积式运行时 Vault 作用域选中切换 Vault 后预览保持可靠且当前进程内仍保持 Vault-only 访问继续 forbid 之前的 Vault否决表面更严格但 Tauri 的 forbid 不可逆切回后合法预览直接失败即本次要修复的 bug放行全部文件系统路径否决能避免预览失败但回到 ADR-0074 有意移除的宽泛 asset protocol 访问违背最小权限方向用自定义协议替换convertFileSrc否决现阶段可以支持精确的“仅活跃 Vault”撤销但对编辑器图片、文件预览、PDF 渲染是一次跨切面的大迁移成本不成比例这张表的核心信息是“运行时可撤销的目录级 allow”在当前 Tauri 版本中不存在因此在“不可逆的 forbid”与“不可撤销的 allow”之间选择了后者并把真正的安全护栏前移到命令层。两层安全模型资产作用域只管读渲染命令层管住一切变更这是本决策最容易被忽视、也最关键的部分。ADR-0099 明确“命令路径仍然在 Rust 命令层强制活跃 Vault 边界先于读取、写入、外部打开和附件导入执行校验。”对应实现在 src-tauri/src/commands/vault/boundary.rs。VaultBoundary::from_request会同时考虑配置中的活跃 Vault 根与请求携带的vault_path两者都存在且规范路径不一致且请求根未注册时返回Vault path must match the active vault所有文件操作最终落到“路径必须留在活跃 Vault 内”的校验错误信息即Path must stay inside the active vault。src-tauri/src/commands/vault/file_cmds.rs 中的open_vault_file_external用默认应用打开也统一走with_external_file_path边界校验后再opener().open_path(...)。由此形成一个清晰的分层资产协议层累积、只读渲染通道决定 WebView 能通过asset:URL 加载哪些本地文件。累积式授权只会让“切回旧 Vault 的笔记仍能显示图片”不会让任何命令拿到越权读写能力命令层严格、活跃 Vault 边界一切修改型操作写文件、重命名、外部打开、附件导入都先通过VaultBoundary校验与资产作用域无关。ADR-0099 的 Consequences 部分把这一模型概括为“应用进程而非每次 Vault 切换是资产 URL 访问的撤销边界”“活跃 Vault 命令校验仍是变更与默认应用打开的主要护栏”。结果、边界与再评估触发条件把 ADR-0099 的 Consequences 落实到可观察行为上当前会话中任何被加载过的 Vault 里的图片和 PDF在切换 Vault 后仍能继续渲染——这正是修复 403 回归的目标行为资产 URL 访问权限随进程退出而清零重启应用后累积列表AllowedAssetScopeRoots重置只有再次加载的 Vault 才会被重新 allow因此“累积”不会跨会话膨胀若未来 Tauri 为 asset protocol 目录提供公开的运行时 unallow 操作应重新评估本决策——ADR 原文以此作为显式的再评估触发条件Re-evaluate this if Tauri adds a public runtime unallow operation for asset protocol directories。对桌面端开发者的启示可以归纳为三点第一使用 Tauri 文件系统/资产作用域时forbid是单向阀门设计权限模型前必须确认“能否撤销”第二当底层框架无法提供细粒度撤销时宁可把宽松面限制在只读渲染通道把强校验放在命令层第三把“进程生命周期”作为撤销边界时要确保状态如本文的AllowedAssetScopeRoots是进程级而非持久化的避免累积跨会话泄漏。延伸阅读决策原文docs/adr/0099-cumulative-vault-asset-scope.md被部分取代的前置决策docs/adr/0074-explicit-external-ai-tool-setup-and-least-privilege-desktop-scope.md核心实现src-tauri/src/asset_scope.rs、src-tauri/src/commands/vault/boundary.rs、src-tauri/src/commands/vault/file_cmds.rs、src-tauri/src/commands/vault/scan_cmds.rs静态配置src-tauri/tauri.conf.json第 50-53 行assetProtocol空 scope测试src-tauri/src/lib_tests.rs第 148-173 行作用域根与累积语义测试前端触发点src/hooks/useNoteWindowLifecycle.ts第 52-59 行【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考