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:
@@ -3,10 +3,13 @@ use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::ToolCallInput;
|
||||
use crate::routes::command_support::execute_runtime_command_via_convex_with_artifacts;
|
||||
use crate::routes::ensure_local_workspace_access;
|
||||
use bridge_runtime::{
|
||||
RuntimeActorWire, RuntimeCommandEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub async fn create_summary(
|
||||
state: &AppState,
|
||||
@@ -71,6 +74,85 @@ async fn create_artifact_node(
|
||||
format!("ai_note_{}_{}", document_id, context.trace.request_id)
|
||||
};
|
||||
|
||||
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)
|
||||
})?;
|
||||
ensure_local_workspace_access(context, &root_uri)
|
||||
.map_err(|error| error.with_context(context))?;
|
||||
if input.dry_run.unwrap_or(false) {
|
||||
return Ok(json!({
|
||||
"dryRun": true,
|
||||
"commandName": "tree.node.create",
|
||||
"commandId": command_id,
|
||||
"artifactType": node_type,
|
||||
"artifactDocumentId": artifact_document_id,
|
||||
"documentId": document_id,
|
||||
"workspaceId": workspace_id,
|
||||
"diff": [{"op": "create_artifact", "artifactType": node_type}]
|
||||
}));
|
||||
}
|
||||
let root_path = parse_local_root_path(&root_uri)?;
|
||||
let artifact_dir = root_path.join(".mnote").join("artifacts");
|
||||
fs::create_dir_all(&artifact_dir).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"local_artifact_write_failed",
|
||||
format!(
|
||||
"无法创建本地 artifact 目录 {}: {error}",
|
||||
artifact_dir.display()
|
||||
),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
let artifact_path = artifact_dir.join(format!(
|
||||
"{}.json",
|
||||
sanitize_local_artifact_file_name(&artifact_document_id)
|
||||
));
|
||||
let artifact_value = json!({
|
||||
"schema": "mnote.local_artifact.v1",
|
||||
"artifactType": node_type,
|
||||
"artifactDocumentId": artifact_document_id,
|
||||
"documentId": document_id,
|
||||
"workspaceId": workspace_id,
|
||||
"content": content,
|
||||
"createdAt": context.trace.trace_id,
|
||||
});
|
||||
fs::write(
|
||||
&artifact_path,
|
||||
serde_json::to_string_pretty(&artifact_value).map_err(|error| {
|
||||
WebError::internal(format!("本地 artifact 序列化失败: {error}"))
|
||||
.with_context(context)
|
||||
})?,
|
||||
)
|
||||
.map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"local_artifact_write_failed",
|
||||
format!(
|
||||
"无法写入本地 artifact 文件 {}: {error}",
|
||||
artifact_path.display()
|
||||
),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
return Ok(json!({
|
||||
"dryRun": false,
|
||||
"commandName": "tree.node.create",
|
||||
"commandId": command_id,
|
||||
"source": "local_folder",
|
||||
"artifactType": node_type,
|
||||
"artifactDocumentId": artifact_document_id,
|
||||
"documentId": document_id,
|
||||
"workspaceId": workspace_id,
|
||||
"result": {
|
||||
"ok": true,
|
||||
"source": "local_folder",
|
||||
"artifactPath": artifact_path,
|
||||
"artifactDocumentId": artifact_document_id,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if input.dry_run.unwrap_or(false) {
|
||||
return Ok(json!({
|
||||
"dryRun": true,
|
||||
@@ -166,6 +248,33 @@ async fn create_artifact_node(
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_local_root_path(root_uri: &str) -> Result<PathBuf, WebError> {
|
||||
let root_path = if let Some(stripped) = root_uri.trim().strip_prefix("file://") {
|
||||
stripped.trim()
|
||||
} else {
|
||||
root_uri.trim()
|
||||
};
|
||||
if root_path.is_empty() {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_folder_root_required",
|
||||
"缺少本地文件夹 rootUri",
|
||||
));
|
||||
}
|
||||
Ok(PathBuf::from(root_path))
|
||||
}
|
||||
|
||||
fn sanitize_local_artifact_file_name(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.map(|ch| match ch {
|
||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||
_ => ch,
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn ensure_write_contract(context: &RequestContext, input: &ToolCallInput) -> Result<(), WebError> {
|
||||
if !input.has_idempotency_key() {
|
||||
return Err(WebError::bad_request_code(
|
||||
|
||||
Reference in New Issue
Block a user