From 270e77a9c81d762c2e3a19c761e239e5fa58ec03 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Tue, 8 Sep 2026 21:50:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20=E6=9A=B4=E9=9C=B2=E9=99=90?= =?UTF-8?q?=E5=AE=9A=E8=8C=83=E5=9B=B4=E7=9A=84=E6=89=A9=E5=B1=95=E4=BF=A1?= =?UTF-8?q?=E4=BB=BB=E5=AE=A1=E9=98=85=E4=B8=8E=E5=AE=89=E8=A3=85=E9=A2=84?= =?UTF-8?q?=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OpenNexus生产化实施进度-2026-09-08.md | 9 + frontend/src-tauri/build.rs | 3 + frontend/src-tauri/capabilities/main.json | 5 +- .../extension_install_preview.toml | 11 + .../extension_trust_confirm.toml | 11 + .../autogenerated/extension_trust_review.toml | 11 + frontend/src-tauri/src/extension_commands.rs | 238 ++++++++++++++++++ frontend/src-tauri/src/main.rs | 17 ++ 8 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 frontend/src-tauri/permissions/autogenerated/extension_install_preview.toml create mode 100644 frontend/src-tauri/permissions/autogenerated/extension_trust_confirm.toml create mode 100644 frontend/src-tauri/permissions/autogenerated/extension_trust_review.toml create mode 100644 frontend/src-tauri/src/extension_commands.rs diff --git a/docs/development/OpenNexus生产化实施进度-2026-09-08.md b/docs/development/OpenNexus生产化实施进度-2026-09-08.md index 38dc1e0..eba5b78 100644 --- a/docs/development/OpenNexus生产化实施进度-2026-09-08.md +++ b/docs/development/OpenNexus生产化实施进度-2026-09-08.md @@ -260,3 +260,12 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写 - install_confirmed 首次执行重新生成预览并比较确认摘要,再进入在线复核。schema 6 保存操作 ID 对应的请求摘要、确认摘要和冻结切换组,网络或进程中断重试沿用原组;改变请求/确认并重用 ID 拒绝。重试仍经过来源/撤销/归档/配置检查和活动 revision CAS。 - 31 项扩展回归与全目标 Clippy -D warnings 通过,日志 `.build/extension-install-preview-tests.log`。新增预览重开摘要稳定、应用版本/活动状态变化使预览失效、错误确认在联网前失败、未知配置拒绝,以及确认计划重开重放不重新选择内容的测试。 - 真实主窗口确认 UI、Host 注入的应用/平台信息、旧实例停机、沙箱迁移和健康探测仍待接入。确认摘要 API 本身不证明用户授权;本轮没有执行第三方包或宣称整体生产化完成。 + + +## 增量:受控桌面信任确认与预览命令 + +- Host 启动时打开应用数据目录 extensions-host 安装库并持有独占锁;新增 extension_trust_review / extension_trust_confirm / extension_install_preview 命令,Tauri ACL 仅授予本地主窗口,命令额外核对 main 标签,未增加远程来源权限。 +- 信任复核把完整候选、原 revision 和摘要保存在 Host 内存,返回随机 review_id。最多 64 个待确认项、两分钟单调时钟有效期;确认仅接收 ID 和摘要,不能替换候选。成功消耗凭据,过期/重用/不匹配及并发设置改变拒绝。 +- 安装预览由 Host 注入应用版本、OS 与架构,renderer 只提交根包、当前 Vault 和配置;后台阻塞任务中保持 Vault 绑定稳定并调用完整预览。尚未开放实际安装执行命令。 +- 桌面二进制 5 项测试和全目标 Clippy -D warnings 通过,日志 `.build/extension-commands-tests.log`。新增确认凭据生命周期与并发冲突测试;ACL 文件及生成权限项已更新。 +- 前端确认对话框、桌面包下载/暂存入口、安装执行编排、沙箱及真实 UI 验收尚待完成;extensions capability 仍关闭。本轮不宣称生产化或 D-04 完成。 diff --git a/frontend/src-tauri/build.rs b/frontend/src-tauri/build.rs index 9c5ff10..a03a5ad 100644 --- a/frontend/src-tauri/build.rs +++ b/frontend/src-tauri/build.rs @@ -16,6 +16,9 @@ fn main() { tauri_build::try_build(tauri_build::Attributes::new().app_manifest( tauri_build::AppManifest::new().commands(&[ "host_capabilities", + "extension_trust_review", + "extension_trust_confirm", + "extension_install_preview", "record_get", "record_write", "sync_login", diff --git a/frontend/src-tauri/capabilities/main.json b/frontend/src-tauri/capabilities/main.json index a80de52..0e76e44 100644 --- a/frontend/src-tauri/capabilities/main.json +++ b/frontend/src-tauri/capabilities/main.json @@ -49,6 +49,9 @@ "allow-sync-run", "allow-sync-preview", "allow-record-get", - "allow-record-write" + "allow-record-write", + "allow-extension-trust-review", + "allow-extension-trust-confirm", + "allow-extension-install-preview" ] } diff --git a/frontend/src-tauri/permissions/autogenerated/extension_install_preview.toml b/frontend/src-tauri/permissions/autogenerated/extension_install_preview.toml new file mode 100644 index 0000000..1f2e13f --- /dev/null +++ b/frontend/src-tauri/permissions/autogenerated/extension_install_preview.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-extension-install-preview" +description = "Enables the extension_install_preview command without any pre-configured scope." +commands.allow = ["extension_install_preview"] + +[[permission]] +identifier = "deny-extension-install-preview" +description = "Denies the extension_install_preview command without any pre-configured scope." +commands.deny = ["extension_install_preview"] diff --git a/frontend/src-tauri/permissions/autogenerated/extension_trust_confirm.toml b/frontend/src-tauri/permissions/autogenerated/extension_trust_confirm.toml new file mode 100644 index 0000000..fa34cc5 --- /dev/null +++ b/frontend/src-tauri/permissions/autogenerated/extension_trust_confirm.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-extension-trust-confirm" +description = "Enables the extension_trust_confirm command without any pre-configured scope." +commands.allow = ["extension_trust_confirm"] + +[[permission]] +identifier = "deny-extension-trust-confirm" +description = "Denies the extension_trust_confirm command without any pre-configured scope." +commands.deny = ["extension_trust_confirm"] diff --git a/frontend/src-tauri/permissions/autogenerated/extension_trust_review.toml b/frontend/src-tauri/permissions/autogenerated/extension_trust_review.toml new file mode 100644 index 0000000..eeebe01 --- /dev/null +++ b/frontend/src-tauri/permissions/autogenerated/extension_trust_review.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-extension-trust-review" +description = "Enables the extension_trust_review command without any pre-configured scope." +commands.allow = ["extension_trust_review"] + +[[permission]] +identifier = "deny-extension-trust-review" +description = "Denies the extension_trust_review command without any pre-configured scope." +commands.deny = ["extension_trust_review"] diff --git a/frontend/src-tauri/src/extension_commands.rs b/frontend/src-tauri/src/extension_commands.rs new file mode 100644 index 0000000..389dd5a --- /dev/null +++ b/frontend/src-tauri/src/extension_commands.rs @@ -0,0 +1,238 @@ +//! Only the local main window may review Host trust or prepared installations. +use super::Host; +use notesagent_host::extension_store::{ExtensionStore, InstallRequest, TrustSetting}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::{ + collections::HashMap, + sync::Mutex, + time::{Duration, Instant}, +}; +use tauri::{State, WebviewWindow}; + +pub struct Review { + setting: TrustSetting, + expected: Option, + fingerprint: String, + expires: Instant, +} +#[derive(Default)] +pub struct Reviews(pub Mutex>); +fn main_window(window: &WebviewWindow) -> Result<(), String> { + if window.label() != "main" { + return Err("EXTENSION_WINDOW_DENIED".into()); + } + Ok(()) +} +fn store( + host: &Host, + f: impl FnOnce(&mut ExtensionStore) -> notesagent_host::workspace::Result, +) -> Result { + let mut guard = host.extensions.lock().map_err(|_| "HOST_BUSY")?; + f(guard.as_mut().ok_or("EXTENSIONS_NOT_READY")?).map_err(|e| e.code) +} +#[tauri::command] +pub fn extension_trust_review( + window: WebviewWindow, + host: State<'_, Host>, + setting: TrustSetting, +) -> Result { + main_window(&window)?; + trust_review(&host, setting) +} +fn trust_review(host: &Host, setting: TrustSetting) -> Result { + let fingerprint = setting.fingerprint().map_err(|e| e.code)?; + let previous = store(host, |s| { + s.trust_setting(&setting.source, &setting.namespace, &setting.key_id) + })?; + let expected = previous + .as_ref() + .map(TrustSetting::fingerprint) + .transpose() + .map_err(|e| e.code)?; + let mut reviews = host.extension_reviews.0.lock().map_err(|_| "HOST_BUSY")?; + reviews.retain(|_, r| r.expires > Instant::now()); + if reviews.len() >= 64 { + return Err("EXTENSION_REVIEW_LIMIT".into()); + } + let id = uuid::Uuid::new_v4().to_string(); + reviews.insert( + id.clone(), + Review { + setting: setting.clone(), + expected, + fingerprint: fingerprint.clone(), + expires: Instant::now() + Duration::from_secs(120), + }, + ); + Ok(json!({"review_id":id,"fingerprint":fingerprint,"previous":previous,"proposed":setting})) +} +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Confirmation { + review_id: String, + fingerprint: String, +} +#[tauri::command] +pub fn extension_trust_confirm( + window: WebviewWindow, + host: State<'_, Host>, + request: Confirmation, +) -> Result { + main_window(&window)?; + trust_confirm(&host, request) +} +fn trust_confirm(host: &Host, request: Confirmation) -> Result { + let mut reviews = host.extension_reviews.0.lock().map_err(|_| "HOST_BUSY")?; + let review = reviews + .get(&request.review_id) + .ok_or("EXTENSION_REVIEW_EXPIRED")?; + if review.expires <= Instant::now() { + reviews.remove(&request.review_id); + return Err("EXTENSION_REVIEW_EXPIRED".into()); + } + if review.fingerprint != request.fingerprint { + return Err("EXTENSION_TRUST_CONFIRMATION".into()); + } + let revision = store(host, |s| { + s.confirm_trust( + &review.setting, + review.expected.as_deref(), + &review.fingerprint, + ) + })?; + reviews.remove(&request.review_id); + Ok(revision) +} +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Preview { + root_key: String, + vault_id: String, + configurations: std::collections::BTreeMap, +} +#[tauri::command] +pub async fn extension_install_preview( + window: WebviewWindow, + host: State<'_, Host>, + request: Preview, +) -> Result { + main_window(&window)?; + let workspace = host.workspace.clone(); + let extensions = host.extensions.clone(); + tauri::async_runtime::spawn_blocking(move || { + // Keep the workspace binding stable until this preview finishes. + let workspace = workspace.lock().map_err(|_| "HOST_BUSY")?; + if workspace.as_ref().ok_or("VAULT_NOT_OPEN")?.vault_id != request.vault_id { + return Err("VAULT_CHANGED".into()); + } + let mut store = extensions.lock().map_err(|_| "HOST_BUSY")?; + let request = InstallRequest { + root_key: request.root_key, + vault_id: request.vault_id, + app_version: env!("CARGO_PKG_VERSION").into(), + platform: std::env::consts::OS.into(), + architecture: std::env::consts::ARCH.into(), + configurations: request.configurations, + }; + let preview = store + .as_mut() + .ok_or("EXTENSIONS_NOT_READY")? + .installation_preview(&request) + .map_err(|e| e.code)?; + serde_json::to_value(preview).map_err(|_| "EXTENSION_PREVIEW_INVALID".into()) + }) + .await + .map_err(|_| "EXTENSION_PREVIEW_FAILED".to_string())? +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn review_nonce_expiry_consumption_and_stale_setting_are_enforced() { + let temp = tempfile::tempdir().unwrap(); + let host = Host::default(); + *host.extensions.lock().unwrap() = Some(ExtensionStore::open(temp.path()).unwrap()); + let setting = TrustSetting { + source: "https://catalog.example/".into(), + source_id: "catalog".into(), + namespace: "examples".into(), + key_id: "key".into(), + public_key: ed25519_dalek::SigningKey::from_bytes(&[7; 32]) + .verifying_key() + .to_bytes(), + enabled: true, + }; + let review = trust_review(&host, setting.clone()).unwrap(); + let id = review["review_id"].as_str().unwrap(); + let fingerprint = review["fingerprint"].as_str().unwrap(); + assert!(trust_confirm( + &host, + Confirmation { + review_id: id.into(), + fingerprint: "wrong".into() + } + ) + .is_err()); + trust_confirm( + &host, + Confirmation { + review_id: id.into(), + fingerprint: fingerprint.into(), + }, + ) + .unwrap(); + assert!(trust_confirm( + &host, + Confirmation { + review_id: id.into(), + fingerprint: fingerprint.into() + } + ) + .is_err()); + let mut changed = setting.clone(); + changed.enabled = false; + let review = trust_review(&host, changed).unwrap(); + let id = review["review_id"].as_str().unwrap(); + host.extension_reviews + .0 + .lock() + .unwrap() + .get_mut(id) + .unwrap() + .expires = Instant::now() - Duration::from_secs(1); + assert!(trust_confirm( + &host, + Confirmation { + review_id: id.into(), + fingerprint: review["fingerprint"].as_str().unwrap().into() + } + ) + .is_err()); + assert!(host.extension_reviews.0.lock().unwrap().get(id).is_none()); + let mut changed = setting.clone(); + changed.public_key = ed25519_dalek::SigningKey::from_bytes(&[8; 32]) + .verifying_key() + .to_bytes(); + let review = trust_review(&host, changed).unwrap(); + let mut concurrent = setting.clone(); + concurrent.enabled = false; + store(&host, |s| { + s.confirm_trust( + &concurrent, + Some(&setting.fingerprint()?), + &concurrent.fingerprint()?, + ) + }) + .unwrap(); + assert!(trust_confirm( + &host, + Confirmation { + review_id: review["review_id"].as_str().unwrap().into(), + fingerprint: review["fingerprint"].as_str().unwrap().into() + } + ) + .is_err()); + } +} diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index feb8111..4920755 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -2,6 +2,9 @@ //! 预览 Host 只开放本地文件命令;未接通的 AI / 同步 / 凭据能力明确返回不可用。 +mod extension_commands; +use extension_commands::*; + mod record_commands; use record_commands::*; mod sync_commands; @@ -23,6 +26,8 @@ use zeroize::Zeroizing; #[derive(Default)] struct Host { requests: Requests, + extensions: Arc>>, + extension_reviews: extension_commands::Reviews, sync: Arc, workspace: Arc>>, recent: Mutex>, @@ -708,6 +713,15 @@ fn main() { .lock() .map_err(|_| std::io::Error::other("HOST_BUSY"))? = Some(RecentVaultStore::open(&state_path).map_err(std::io::Error::other)?); + let extension_root = app.path().app_data_dir()?.join("extensions-host"); + std::fs::create_dir_all(&extension_root)?; + *app.state::() + .extensions + .lock() + .map_err(|_| std::io::Error::other("HOST_BUSY"))? = Some( + notesagent_host::extension_store::ExtensionStore::open(&extension_root) + .map_err(|error| std::io::Error::other(error.code))?, + ); let credential_state = app.state::().credentials.clone(); *credential_state .lock() @@ -833,6 +847,9 @@ fn main() { }) .invoke_handler(tauri::generate_handler![ host_capabilities, + extension_trust_review, + extension_trust_confirm, + extension_install_preview, record_get, record_write, sync_login,