feat: advance local-first workspace checklist

- add admin access-policy UI and local access control surfaces

- add local markdown conflict resolution UI and smoke coverage

- add ACP local agent changed-files audit scaffold and read-only write guard

- document current P0-P2 checklist progress and verification evidence
This commit is contained in:
lix-2026
2026-05-19 08:07:17 +08:00
parent a2cb1338c8
commit 68d321e297
36 changed files with 8643 additions and 235 deletions
+81 -2
View File
@@ -18,14 +18,17 @@ pub async fn page_get(
WebError::bad_request_code("mnote_tool_bad_request", "mnote.page.get 缺少 documentId")
.with_context(context)
})?;
crate::hermes_tools::doc::ensure_ai_scope_resource_allowed(context, input, &document_id)?;
let workspace_id = input.effective_workspace_id();
let source_kind = input.effective_source_kind();
let root_uri = input.effective_root_uri();
let aggregate = build_page_aggregate_snapshot(
state,
context,
&document_id,
workspace_id.as_deref(),
None,
None,
source_kind.as_deref(),
root_uri.as_deref(),
)
.await?;
let aggregate_value =
@@ -83,6 +86,14 @@ fn ensure_write_contract(context: &RequestContext, input: &ToolCallInput) -> Res
)
.with_context(context));
}
if input.ai_access_scope_is_read_only() {
return Err(WebError::new(
axum::http::StatusCode::FORBIDDEN,
"mnote_tool_ai_scope_write_forbidden",
"当前 AI scope 是只读权限,禁止执行写入型 mnote tool",
)
.with_context(context));
}
Ok(())
}
@@ -200,6 +211,74 @@ async fn page_command(
return Ok(result);
}
if input.effective_source_kind().as_deref() == Some("local_folder") {
let root_uri = input.effective_root_uri().ok_or_else(|| {
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
.with_context(context)
})?;
crate::routes::ensure_local_workspace_access(context, &root_uri)
.map_err(|error| error.with_context(context))?;
let local_result = match command_name {
"page.body.save" => {
let content = payload.get("content").cloned().unwrap_or(Value::Null);
crate::routes::write_local_markdown_page_body(
&core_protocol::PageBodyWriteRequest {
document_id: document_id.clone(),
workspace_id: workspace_id.clone().unwrap_or_default(),
source_kind: core_protocol::WorkspaceSourceKind::LocalFolder,
root_uri: root_uri.clone(),
expected_file_version: input.arg_string("expectedFileVersion"),
base_content_hash: input.arg_string("baseContentHash"),
content_format: "editorBlocks".into(),
content,
editor_source: Some("mnote.page.save".into()),
},
)?
}
"page.head.updateTitle" => {
let title = payload
.get("title")
.and_then(Value::as_str)
.ok_or_else(|| {
WebError::bad_request_code(
"mnote_tool_bad_request",
"mnote.page.update_title 缺少 title",
)
.with_context(context)
})?;
crate::routes::update_local_markdown_title(&root_uri, &document_id, title)?
}
"page.layout.updateOptions" => {
let options = payload.get("options").cloned().ok_or_else(|| {
WebError::bad_request_code(
"mnote_tool_bad_request",
"mnote.page.update_options 缺少 options",
)
.with_context(context)
})?;
crate::routes::update_local_page_options(&root_uri, &document_id, &options)?
}
_ => {
return Err(WebError::bad_request_code(
"mnote_tool_bad_request",
format!("local source 暂不支持页面命令 {command_name}"),
)
.with_context(context));
}
};
let mut result = json!({
"dryRun": false,
"source": "local_folder",
"commandName": if command_name == "page.body.save" { "page.body.write" } else { command_name },
"commandId": command_id,
"documentId": document_id,
"workspaceId": workspace_id,
"result": local_result
});
merge_result_extra(&mut result, result_extra);
return Ok(result);
}
let command = RuntimeCommandEnvelopeWire {
name: command_name.into(),
command_id: command_id.clone(),