chore: align local-first control plane and editor fixes
- wire SQLite control-plane access/session paths into Rust web local-folder routes - preserve local Markdown attachment semantics across upload, reload, and secondary-pane resource tabs - refresh design governance docs, Reasonix task templates, and bug records - retire root .mcp.json local MCP config
This commit is contained in:
@@ -6,10 +6,10 @@ use crate::routes::command_support::{
|
||||
execute_runtime_command_via_convex_with_artifacts, read_optional_non_empty,
|
||||
};
|
||||
use crate::routes::local_folder_source::{
|
||||
ensure_local_workspace_access, ensure_local_workspace_read_access,
|
||||
ensure_local_workspace_access_with_state, ensure_local_workspace_read_access_with_state,
|
||||
execute_local_tree_command_with_sort, load_local_folder_file_tree_snapshot,
|
||||
load_local_folder_page_tree_snapshot, local_folder_watch_revision,
|
||||
local_workspace_id_from_root_uri,
|
||||
local_workspace_id_from_root_uri, LocalAccessMode,
|
||||
};
|
||||
use crate::routes::query_support::{
|
||||
fetch_documents_meta_via_convex, resolve_effective_workspace_id,
|
||||
@@ -6084,10 +6084,11 @@ fn json_response(context: &RequestContext, result: Value) -> (StatusCode, Json<V
|
||||
}
|
||||
|
||||
pub async fn local_folder_watch(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<LocalFolderWatchQuery>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
ensure_local_workspace_read_access(&context, &query.root_uri)
|
||||
ensure_local_workspace_read_access_with_state(&state, &context, &query.root_uri)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let revision = local_folder_watch_revision(&query.root_uri)?;
|
||||
Ok(json_response(
|
||||
@@ -6185,7 +6186,7 @@ pub async fn tree_shell(
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
|
||||
})?;
|
||||
ensure_local_workspace_read_access(&effective_context, root_uri)
|
||||
ensure_local_workspace_read_access_with_state(&state, &effective_context, root_uri)
|
||||
.map_err(|error| error.with_context(&effective_context))?;
|
||||
(
|
||||
local_workspace_id_from_root_uri(root_uri)?,
|
||||
@@ -6808,8 +6809,13 @@ pub async fn tree_command(
|
||||
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
|
||||
.with_context(&context)
|
||||
})?;
|
||||
ensure_local_workspace_access(&context, root_uri)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
ensure_local_workspace_access_with_state(
|
||||
&state,
|
||||
&context,
|
||||
root_uri,
|
||||
LocalAccessMode::Write,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let execution = execute_local_tree_command_with_sort(
|
||||
root_uri,
|
||||
action,
|
||||
@@ -6932,6 +6938,7 @@ mod tests {
|
||||
use crate::routes::command_support::build_runtime_command_plan;
|
||||
use axum::body::Body;
|
||||
use axum::http::{HeaderMap, HeaderValue, Method, Request, StatusCode, Uri};
|
||||
use control_plane::{DirectoryGrantInput, UpsertUserInput};
|
||||
use serde_json::Value;
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
@@ -7223,6 +7230,88 @@ mod tests {
|
||||
assert!(html.contains("data-document-id=\"local-md:README.md\""));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tree_shell_local_folder_allows_sqlite_directory_read_grant() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-folder-sqlite-read-grant-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).expect("create local root");
|
||||
std::fs::write(root.join("README.md"), "# Shared\n").expect("write local md");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
init_local_workspace(&root, "owner_user");
|
||||
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: true,
|
||||
enable_editor_actor: true,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
});
|
||||
for (user_id, role) in [("shujuan", None), ("liaibo", Some("admin"))] {
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some(user_id.into()),
|
||||
email: Some(format!("{user_id}@example.com")),
|
||||
username: user_id.into(),
|
||||
display_name: user_id.into(),
|
||||
role: role.map(str::to_string),
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert grant user");
|
||||
}
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "shujuan".into(),
|
||||
workspace_id: None,
|
||||
root_uri: root_uri.clone(),
|
||||
root_path: root
|
||||
.canonicalize()
|
||||
.expect("canonical root")
|
||||
.display()
|
||||
.to_string(),
|
||||
permission: "read".into(),
|
||||
recursive: true,
|
||||
capabilities: vec![],
|
||||
source: "admin".into(),
|
||||
created_by: Some("liaibo".into()),
|
||||
})
|
||||
.expect("grant sqlite read");
|
||||
|
||||
let response = build_app(state)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(format!(
|
||||
"/tree?mode=filetree&sourceKind=local_folder&rootUri={root_uri}"
|
||||
))
|
||||
.header("x-mnote-actor-id", "shujuan")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_folder_tree_shell_does_not_reload_page_for_refresh() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
@@ -7778,6 +7867,89 @@ mod tests {
|
||||
assert_eq!(payload["code"], "local_workspace_access_denied");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tree_command_local_folder_allows_sqlite_directory_write_grant() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-tree-sqlite-write-grant-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).expect("create local root");
|
||||
init_local_workspace(&root, "owner_user");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: true,
|
||||
enable_editor_actor: true,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
});
|
||||
for (user_id, role) in [("shujuan", None), ("liaibo", Some("admin"))] {
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some(user_id.into()),
|
||||
email: Some(format!("{user_id}@example.com")),
|
||||
username: user_id.into(),
|
||||
display_name: user_id.into(),
|
||||
role: role.map(str::to_string),
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert grant user");
|
||||
}
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "shujuan".into(),
|
||||
workspace_id: None,
|
||||
root_uri: root_uri.clone(),
|
||||
root_path: root
|
||||
.canonicalize()
|
||||
.expect("canonical root")
|
||||
.display()
|
||||
.to_string(),
|
||||
permission: "write".into(),
|
||||
recursive: true,
|
||||
capabilities: vec![],
|
||||
source: "admin".into(),
|
||||
created_by: Some("liaibo".into()),
|
||||
})
|
||||
.expect("grant sqlite write");
|
||||
|
||||
let response = build_app(state)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/api/tree/commands")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "shujuan")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(format!(
|
||||
r#"{{"action":"create","sourceKind":"local_folder","rootUri":"{root_uri}","title":"授权新页面"}}"#
|
||||
)))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tree_shell_embeds_renderer_input_contract() {
|
||||
let filetree_response = app()
|
||||
|
||||
Reference in New Issue
Block a user