fix(host): 允许布局记录通过实际命令策略
This commit is contained in:
@@ -751,3 +751,13 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写
|
|||||||
- 启动断言失败时增加耗时、授权错误机器码、活动进程数诊断,不输出 argv、环境、凭据或用户路径,便于后续区别许可撤销、资源终止与程序自身异常。
|
- 启动断言失败时增加耗时、授权错误机器码、活动进程数诊断,不输出 argv、环境、凭据或用户路径,便于后续区别许可撤销、资源终止与程序自身异常。
|
||||||
- 第一轮默认并行 desktop library 回归 129 通过、10 ignored,21.72 秒,含实际正常启动、网络和短期许可到期用例,日志 .build/native-budget-parallel-1.log;后续重复结果另列。
|
- 第一轮默认并行 desktop library 回归 129 通过、10 ignored,21.72 秒,含实际正常启动、网络和短期许可到期用例,日志 .build/native-budget-parallel-1.log;后续重复结果另列。
|
||||||
- 第 2、3 轮同样默认并行运行,各 129 通过、10 ignored,分别 21.24 与 20.88 秒,日志 .build/native-budget-parallel-2.log、.build/native-budget-parallel-3.log。三轮共 387 次通过执行,不能将重复执行数当作独立测试数量。全目标 Clippy -D warnings 通过(.build/native-budget-clippy.log)。本轮仅修改原生测试探针预算和诊断,不改变生产安全策略;历史偶发失败根因未获得完整现场证明,整个生产化目标仍未完成。
|
- 第 2、3 轮同样默认并行运行,各 129 通过、10 ignored,分别 21.24 与 20.88 秒,日志 .build/native-budget-parallel-2.log、.build/native-budget-parallel-3.log。三轮共 387 次通过执行,不能将重复执行数当作独立测试数量。全目标 Clippy -D warnings 通过(.build/native-budget-clippy.log)。本轮仅修改原生测试探针预算和诊断,不改变生产安全策略;历史偶发失败根因未获得完整现场证明,整个生产化目标仍未完成。
|
||||||
|
|
||||||
|
|
||||||
|
## 增量:修复布局记录的桌面命令白名单遗漏
|
||||||
|
|
||||||
|
- 检查用户 Skill 的保存路径时,发现 record_commands::preference 仍只允许 theme_settings/preferences,遗漏已实现的 layout。之前的 Rust Workspace/传输测试绕过此主窗口策略,前端又 mock 了 hostInvoke,因而未能证明真实布局命令可用;此前“布局同步已接通”的证据对此边界不完整。
|
||||||
|
- 增加 layout 白名单,保留 persona/Task/凭据等其他类别拒绝。record_get/write 共用可直接测试的 get_record/write_record,实际 Tauri command 也调用相同函数;校验 Vault、kind/id、schema 和 journal 的生产路径不再由测试替代。
|
||||||
|
- 两项 Host 主程序测试通过:布局真实策略写入与同操作重放、关闭/重开读取同一记录、跨 Vault 拒绝且 outbox 不额外增加,以及禁止把偏好命令扩展成任意记录访问器、未知 token 字段在 journal 前拒绝。日志 .build/layout-command-boundary.log。
|
||||||
|
- 修复同时解除首次上传/合并 seedCurrentPreferences 中准备布局记录时的 RECORD_SCOPE_DENIED 障碍;可选范围过滤仍由独立 sync_scope 决定,允许本机编辑不等于授权上传。尚未补齐用户 Skill 编辑/逻辑记录,本轮优先修复实际边界缺陷。
|
||||||
|
|
||||||
|
- 完整 desktop/all-targets Rust 回归 146 通过、12 ignored(library 129、Host 10、其他集成 7),包括真实 Core 桥接与 Sync 服务集成;Clippy --all-targets -D warnings 通过。日志 .build/layout-command-full.log 与 .build/layout-command-clippy.log。未执行真正双机桌面 UI 验收,不能据此标记整体生产化完成。
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
//! Main-window preference records; no generic credential or application-state accessor.
|
//! Main-window preference records; no generic credential or application-state accessor.
|
||||||
use super::{with_workspace, Host};
|
use super::{with_workspace, Host};
|
||||||
use notesagent_host::{records, workspace::HostError};
|
use notesagent_host::{
|
||||||
|
records,
|
||||||
|
workspace::{HostError, Result as HostResult, Workspace},
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
@@ -19,43 +22,116 @@ pub struct Write {
|
|||||||
expected: String,
|
expected: String,
|
||||||
operation_id: String,
|
operation_id: String,
|
||||||
}
|
}
|
||||||
fn preference(kind: &str) -> Result<(), String> {
|
fn preference(kind: &str) -> HostResult<()> {
|
||||||
if matches!(kind, "theme_settings" | "preferences") {
|
if matches!(kind, "theme_settings" | "preferences" | "layout") {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err("RECORD_SCOPE_DENIED".into())
|
Err(HostError::new("RECORD_SCOPE_DENIED"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn get_record(ws: &mut Workspace, request: Get) -> HostResult<Option<Value>> {
|
||||||
|
preference(&request.kind)?;
|
||||||
|
if ws.vault_id != request.vault_id {
|
||||||
|
return Err(HostError::new("VAULT_CHANGED"));
|
||||||
|
}
|
||||||
|
ws.record_get_kind(&request.kind, &request.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_record(ws: &mut Workspace, request: Write) -> HostResult<Value> {
|
||||||
|
let kind = request.record["kind"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| HostError::new("RECORD_SCHEMA_INVALID"))?;
|
||||||
|
preference(kind)?;
|
||||||
|
if ws.vault_id != request.vault_id {
|
||||||
|
return Err(HostError::new("VAULT_CHANGED"));
|
||||||
|
}
|
||||||
|
let path = records::path_for(kind, request.record["id"].as_str().unwrap_or(""))?;
|
||||||
|
let bytes =
|
||||||
|
serde_json::to_vec(&request.record).map_err(|_| HostError::new("RECORD_SCHEMA_INVALID"))?;
|
||||||
|
let entry = ws.write_operation(
|
||||||
|
&path,
|
||||||
|
&request.expected,
|
||||||
|
&bytes,
|
||||||
|
"local",
|
||||||
|
&request.operation_id,
|
||||||
|
)?;
|
||||||
|
Ok(json!({"record":request.record,"hash":entry.hash,"file_id":entry.file_id}))
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn record_get(host: State<'_, Host>, request: Get) -> Result<Option<Value>, String> {
|
pub fn record_get(host: State<'_, Host>, request: Get) -> Result<Option<Value>, String> {
|
||||||
preference(&request.kind)?;
|
with_workspace(&host, |ws| get_record(ws, request))
|
||||||
with_workspace(&host, |ws| {
|
|
||||||
if ws.vault_id != request.vault_id {
|
|
||||||
return Err(HostError::new("VAULT_CHANGED"));
|
|
||||||
}
|
|
||||||
ws.record_get_kind(&request.kind, &request.id)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn record_write(host: State<'_, Host>, request: Write) -> Result<Value, String> {
|
pub fn record_write(host: State<'_, Host>, request: Write) -> Result<Value, String> {
|
||||||
let kind = request.record["kind"]
|
with_workspace(&host, |ws| write_record(ws, request))
|
||||||
.as_str()
|
}
|
||||||
.ok_or("RECORD_SCHEMA_INVALID")?;
|
|
||||||
preference(kind)?;
|
#[cfg(test)]
|
||||||
let path =
|
mod tests {
|
||||||
records::path_for(kind, request.record["id"].as_str().unwrap_or("")).map_err(|e| e.code)?;
|
use super::*;
|
||||||
let bytes = serde_json::to_vec(&request.record).map_err(|_| "RECORD_SCHEMA_INVALID")?;
|
#[test]
|
||||||
with_workspace(&host, |ws| {
|
fn layout_traverses_the_actual_command_policy_and_workspace_journal() {
|
||||||
if ws.vault_id != request.vault_id {
|
let root = tempfile::tempdir().unwrap();
|
||||||
return Err(HostError::new("VAULT_CHANGED"));
|
let mut ws = Workspace::open(root.path()).unwrap();
|
||||||
}
|
let record = json!({"schema":1,"kind":"layout","id":"sidebars","data":{"primaryExpanded":true,"workspaceWidth":400,"chatWidth":320}});
|
||||||
let entry = ws.write_operation(
|
let operation = uuid::Uuid::new_v4().to_string();
|
||||||
&path,
|
let make = |vault: &str| Write {
|
||||||
&request.expected,
|
vault_id: vault.into(),
|
||||||
&bytes,
|
record: record.clone(),
|
||||||
"local",
|
expected: String::new(),
|
||||||
&request.operation_id,
|
operation_id: operation.clone(),
|
||||||
)?;
|
};
|
||||||
Ok(json!({"record":request.record,"hash":entry.hash,"file_id":entry.file_id}))
|
let vault = ws.vault_id.clone();
|
||||||
})
|
let first = write_record(&mut ws, make(&vault)).unwrap();
|
||||||
|
assert_eq!(write_record(&mut ws, make(&vault)).unwrap(), first);
|
||||||
|
assert_eq!(ws.pending_count().unwrap(), 1);
|
||||||
|
drop(ws);
|
||||||
|
let mut ws = Workspace::open(root.path()).unwrap();
|
||||||
|
let loaded = get_record(
|
||||||
|
&mut ws,
|
||||||
|
Get {
|
||||||
|
vault_id: vault,
|
||||||
|
kind: "layout".into(),
|
||||||
|
id: "sidebars".into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(loaded, first);
|
||||||
|
assert_eq!(
|
||||||
|
write_record(&mut ws, make("different-vault"))
|
||||||
|
.unwrap_err()
|
||||||
|
.code,
|
||||||
|
"VAULT_CHANGED"
|
||||||
|
);
|
||||||
|
assert_eq!(ws.pending_count().unwrap(), 1);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn preference_commands_do_not_become_a_generic_record_accessor() {
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let mut ws = Workspace::open(root.path()).unwrap();
|
||||||
|
for kind in ["persona", "task", "credentials", "user_skill"] {
|
||||||
|
let request = Get {
|
||||||
|
vault_id: ws.vault_id.clone(),
|
||||||
|
kind: kind.into(),
|
||||||
|
id: "default".into(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
get_record(&mut ws, request).unwrap_err().code,
|
||||||
|
"RECORD_SCOPE_DENIED"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let request = Write {
|
||||||
|
vault_id: ws.vault_id.clone(),
|
||||||
|
record: json!({"schema":1,"kind":"layout","id":"sidebars","data":{"primaryExpanded":true,"workspaceWidth":400,"chatWidth":320,"token":"forbidden"}}),
|
||||||
|
expected: String::new(),
|
||||||
|
operation_id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
write_record(&mut ws, request).unwrap_err().code,
|
||||||
|
"RECORD_SCHEMA_INVALID"
|
||||||
|
);
|
||||||
|
assert_eq!(ws.pending_count().unwrap(), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user