Harden auth/vault path sanitization and clean WeKnora docs

This commit is contained in:
Agent Board
2026-07-28 17:04:27 +08:00
parent 2deaf59f7b
commit 26ff1a9c9a
190 changed files with 13454 additions and 4987 deletions
+12 -6
View File
@@ -255,7 +255,8 @@ impl CommandContext {
editor_dirty: false,
editor_dirty_state: Some("clean".to_string()),
editor_has_selection: false,
ai_can_write: true,
// 最小权限:默认禁止 AI 写;调用方确认需要时再 with_ai_can_write(true)
ai_can_write: false,
}
}
@@ -351,11 +352,15 @@ impl CommandContext {
self
}
/// 用 builder 模式设置 editor dirty。
/// 用 builder 模式设置 editor dirty;与 `editor_dirty_state` 双向同步
pub fn with_editor_dirty(mut self, dirty: bool) -> Self {
self.editor_dirty = dirty;
if dirty && self.editor_dirty_state.as_deref() == Some("clean") {
if dirty {
// dirty=true 时始终落到 dirty 状态(含从 clean/其它状态同步)
self.editor_dirty_state = Some("dirty".to_string());
} else {
// dirty=false 时重置为 clean,避免两字段语义分裂
self.editor_dirty_state = Some("clean".to_string());
}
self
}
@@ -700,10 +705,11 @@ pub fn check_when(ctx: &CommandContext, when_expr: &str) -> Result<bool, String>
/// 检查一个命令在指定上下文中是否应 enabled。
/// 如果 when 表达式为 None 或空,默认返回 true。
/// when 解析失败时 fail-closed:返回 false(禁止因坏表达式默认放开命令)。
pub fn is_command_enabled(ctx: &CommandContext, when: Option<&str>) -> bool {
match when {
None | Some("") => true,
Some(expr) => check_when(ctx, expr).unwrap_or(true),
Some(expr) => check_when(ctx, expr).unwrap_or(false),
}
}
@@ -905,8 +911,8 @@ mod command_context_tests {
#[test]
fn command_context_parse_error_returns_default() {
let ctx = CommandContext::new();
// 解析错误 → is_command_enabled 返回 true(默认允许
assert!(is_command_enabled(&ctx, Some("invalid syntax &&&")));
// 解析错误 → fail-closedis_command_enabled 返回 false(禁止默认放开
assert!(!is_command_enabled(&ctx, Some("invalid syntax &&&")));
}
#[test]
+23 -1
View File
@@ -60,7 +60,14 @@ impl JobTicket {
}
pub fn decide_access(context: &AccessContext) -> AccessDecision {
if context.tenant_id.is_none() {
// None 与空串/纯空白均视为缺 tenant,避免 `Some("")` 绕过鉴权。
let tenant_missing = context
.tenant_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.is_none();
if tenant_missing {
return AccessDecision::Deny(AccessDenyReason::MissingTenant);
}
if context.workspace_id.trim().is_empty() {
@@ -100,6 +107,21 @@ mod tests {
assert_eq!(decide_access(&context), AccessDecision::Allow);
}
#[test]
fn access_denies_empty_tenant_string() {
let context = AccessContext {
tenant_id: Some(" ".to_string()),
workspace_id: "ws-1".to_string(),
actor_id: "user-1".to_string(),
actor_type: "human".to_string(),
source: "react-next".to_string(),
};
assert_eq!(
decide_access(&context),
AccessDecision::Deny(AccessDenyReason::MissingTenant)
);
}
#[test]
fn job_ticket_keeps_trace_fields() {
let ticket = JobTicket::new(
+24 -1
View File
@@ -246,7 +246,13 @@ impl DocumentBuffer {
pub fn mark_dirty(&mut self, content_hash: String) {
self.current_content_hash = Some(content_hash);
self.dirty_state = DocBufferDirtyState::Dirty;
// Stale / Deleted 不可被普通编辑覆盖:保留外部冲突与删除语义。
match self.dirty_state {
DocBufferDirtyState::Stale | DocBufferDirtyState::Deleted => {}
_ => {
self.dirty_state = DocBufferDirtyState::Dirty;
}
}
}
pub fn mark_saved(&mut self, file_version: String, content_hash: String) {
@@ -298,6 +304,14 @@ impl DocumentBuffer {
pub fn mark_deleted(&mut self) {
self.dirty_state = DocBufferDirtyState::Deleted;
}
/// 从回收站 restore 后清除 deleted 标记,使已打开 buffer 可再次编辑。
/// 恢复为 Clean:文件已回到磁盘,当前 buffer 内容仍视为与 base 一致直至下一次外部变更检测。
pub fn clear_deleted(&mut self) {
if self.dirty_state == DocBufferDirtyState::Deleted {
self.dirty_state = DocBufferDirtyState::Clean;
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -1109,6 +1123,11 @@ mod tests {
assert_eq!(buf.dirty_state, DocBufferDirtyState::Stale);
assert_eq!(buf.external_actor.as_deref(), Some("external-editor"));
// Stale 时继续编辑只更新 content_hash,不得降级回 Dirty。
buf.mark_dirty("sha256:stale-edit".into());
assert_eq!(buf.dirty_state, DocBufferDirtyState::Stale);
assert_eq!(buf.current_content_hash.as_deref(), Some("sha256:stale-edit"));
buf.mark_saved("v2".into(), "sha256:saved".into());
assert!(!buf.is_dirty());
assert_eq!(buf.dirty_state, DocBufferDirtyState::Clean);
@@ -1119,5 +1138,9 @@ mod tests {
buf.mark_deleted();
assert_eq!(buf.dirty_state, DocBufferDirtyState::Deleted);
buf.mark_dirty("sha256:after-delete".into());
assert_eq!(buf.dirty_state, DocBufferDirtyState::Deleted);
buf.clear_deleted();
assert_eq!(buf.dirty_state, DocBufferDirtyState::Clean);
}
}
+2 -1
View File
@@ -77,7 +77,8 @@ pub struct MindmapKernelProjection {
pub source: MindmapProjectionSource,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
// style/refs 含 serde_json::Value(可能含 f64),不实现 Eq;仅保留 PartialEq。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MindmapKernelNode {
pub node_id: String,