fix: expose pi rust advanced diagnostics
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
# Page AI Pi Rust P2 Advanced Runtime Diagnostics Checklist
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
P1 已恢复 Pi Rust 官方主链能力。P2 聚焦非对话主链但会影响稳定性和排障效率的高级能力:运行时 policy 透传、effective policy 可见性、官方诊断/admin 命令的受控接入。
|
||||||
|
|
||||||
|
## 产品取舍
|
||||||
|
|
||||||
|
- [x] 支持 `--extension-policy` / `--repair-policy` 透传;MNote 仍保留自身 permission mode 与 allowlist,但诊断面暴露最终 effective advanced runtime。
|
||||||
|
- [x] 支持 `--session-durability`、`--request-timeout`、`--max-tool-iterations`、`--hide-cwd-in-prompt`;来源为 AI policy 的 `piRuntime` / `advancedRuntime`,env 作为兜底。
|
||||||
|
- [x] provider 默认仍按 MNote 宿主策略走 OmniRoute,不把默认 provider 当 bug;后续如要“官方 Pi Rust 完整 provider passthrough”,应作为单独高级 provider passthrough 项。
|
||||||
|
- [x] 官方 `doctor/context-preview/list/info/search/update-index` 接入 MNote 诊断 API;`update-index` 标记 `mutatesCache=true`,不放到普通对话链路。
|
||||||
|
- [x] 不开放 `install/remove/update/migrate/config` 这类会修改扩展安装或会话结构的命令;诊断 API 不接受裸 argv 透传。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
- [x] `cargo fmt --manifest-path rust/Cargo.toml -p mnote-web`
|
||||||
|
- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_pi -- --nocapture`:32 passed
|
||||||
|
- [x] `node scripts/task-pi-lab-static-smoke.js`:259 checks passed
|
||||||
|
- [x] 真实 Chromium Pi Lab smoke 仍能完成 built-in full access 删除验证:`/tmp/mnote-pi-full-access-builtin-tools-1783809738370/result.json`
|
||||||
|
- [x] `codegraph sync . && codegraph status .`:Index is up to date
|
||||||
|
- [x] 提交 git
|
||||||
@@ -305,6 +305,7 @@ pub(crate) struct AiRuntimeResolvedModel {
|
|||||||
pub(crate) struct EffectiveAiRuntimePolicy {
|
pub(crate) struct EffectiveAiRuntimePolicy {
|
||||||
pub default_model: String,
|
pub default_model: String,
|
||||||
pub allowed_models: Vec<String>,
|
pub allowed_models: Vec<String>,
|
||||||
|
pub pi_runtime: Value,
|
||||||
pub enabled_skills: Vec<String>,
|
pub enabled_skills: Vec<String>,
|
||||||
pub enabled_skill_sources: Vec<String>,
|
pub enabled_skill_sources: Vec<String>,
|
||||||
pub enabled_mcp_servers: Vec<String>,
|
pub enabled_mcp_servers: Vec<String>,
|
||||||
@@ -1997,6 +1998,11 @@ pub(crate) fn load_effective_ai_runtime_policy(
|
|||||||
EffectiveAiRuntimePolicy {
|
EffectiveAiRuntimePolicy {
|
||||||
default_model: normalize_model_ref(None, &default_model).unwrap_or(default_model),
|
default_model: normalize_model_ref(None, &default_model).unwrap_or(default_model),
|
||||||
allowed_models,
|
allowed_models,
|
||||||
|
pi_runtime: model_policy
|
||||||
|
.get("piRuntime")
|
||||||
|
.or_else(|| model_policy.get("advancedRuntime"))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| json!({})),
|
||||||
enabled_skills,
|
enabled_skills,
|
||||||
enabled_skill_sources,
|
enabled_skill_sources,
|
||||||
enabled_mcp_servers,
|
enabled_mcp_servers,
|
||||||
|
|||||||
@@ -605,6 +605,7 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
.route("/api/page-ai/pi/state", post(page_ai_pi::state))
|
.route("/api/page-ai/pi/state", post(page_ai_pi::state))
|
||||||
.route("/api/page-ai/pi/compact", post(page_ai_pi::compact))
|
.route("/api/page-ai/pi/compact", post(page_ai_pi::compact))
|
||||||
.route("/api/page-ai/pi/rpc-command", post(page_ai_pi::rpc_command))
|
.route("/api/page-ai/pi/rpc-command", post(page_ai_pi::rpc_command))
|
||||||
|
.route("/api/page-ai/pi/diagnostics", post(page_ai_pi::diagnostics))
|
||||||
.route(
|
.route(
|
||||||
"/api/page-ai/pi/queue-config",
|
"/api/page-ai/pi/queue-config",
|
||||||
post(page_ai_pi::queue_config),
|
post(page_ai_pi::queue_config),
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const PI_LAB_SCHEMA_STATE: &str = "mnote.page_ai_pi.state.v1";
|
|||||||
const PI_LAB_SCHEMA_COMPACT: &str = "mnote.page_ai_pi.compact.v1";
|
const PI_LAB_SCHEMA_COMPACT: &str = "mnote.page_ai_pi.compact.v1";
|
||||||
const PI_LAB_SCHEMA_QUEUE_CONFIG: &str = "mnote.page_ai_pi.queue_config.v1";
|
const PI_LAB_SCHEMA_QUEUE_CONFIG: &str = "mnote.page_ai_pi.queue_config.v1";
|
||||||
const PI_LAB_SCHEMA_RPC_COMMAND: &str = "mnote.page_ai_pi.rpc_command.v1";
|
const PI_LAB_SCHEMA_RPC_COMMAND: &str = "mnote.page_ai_pi.rpc_command.v1";
|
||||||
|
const PI_LAB_SCHEMA_DIAGNOSTICS: &str = "mnote.page_ai_pi.diagnostics.v1";
|
||||||
const PI_LAB_DEFAULT_MODEL_PROVIDER: &str = "omniroute";
|
const PI_LAB_DEFAULT_MODEL_PROVIDER: &str = "omniroute";
|
||||||
const PI_LAB_SCHEMA_SESSION_TREE: &str = "mnote.page_ai_pi.session_tree.v1";
|
const PI_LAB_SCHEMA_SESSION_TREE: &str = "mnote.page_ai_pi.session_tree.v1";
|
||||||
const PI_LAB_SCHEMA_FORK: &str = "mnote.page_ai_pi.fork.v1";
|
const PI_LAB_SCHEMA_FORK: &str = "mnote.page_ai_pi.fork.v1";
|
||||||
@@ -73,6 +74,7 @@ const PI_LAB_MANAGED_BUILTIN_TOOLS: [&str; 8] = [
|
|||||||
const PI_LAB_MCP_CACHE_FILE: &str = "mcp-cache.json";
|
const PI_LAB_MCP_CACHE_FILE: &str = "mcp-cache.json";
|
||||||
const PI_LAB_MCP_BRIDGE_TIMEOUT_MS: u64 = 45_000;
|
const PI_LAB_MCP_BRIDGE_TIMEOUT_MS: u64 = 45_000;
|
||||||
const PI_LAB_MCP_BRIDGE_MAX_OUTPUT_BYTES: usize = 2 * 1024 * 1024;
|
const PI_LAB_MCP_BRIDGE_MAX_OUTPUT_BYTES: usize = 2 * 1024 * 1024;
|
||||||
|
const PI_LAB_DIAGNOSTICS_MAX_OUTPUT_BYTES: usize = 512 * 1024;
|
||||||
pub const PI_LAB_PROFILE: &str = "pi_lab";
|
pub const PI_LAB_PROFILE: &str = "pi_lab";
|
||||||
pub const PI_LAB_ACP_RUNTIME: &str = "pi";
|
pub const PI_LAB_ACP_RUNTIME: &str = "pi";
|
||||||
|
|
||||||
@@ -171,6 +173,30 @@ struct PiLabUiPendingResponse {
|
|||||||
expires_at_ms: u128,
|
expires_at_ms: u128,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct PiLabAdvancedRuntimeConfig {
|
||||||
|
extension_policy: Option<String>,
|
||||||
|
repair_policy: Option<String>,
|
||||||
|
session_durability: Option<String>,
|
||||||
|
request_timeout_secs: Option<u64>,
|
||||||
|
max_tool_iterations: Option<u64>,
|
||||||
|
hide_cwd_in_prompt: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PiLabAdvancedRuntimeConfig {
|
||||||
|
fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
extension_policy: None,
|
||||||
|
repair_policy: None,
|
||||||
|
session_durability: None,
|
||||||
|
request_timeout_secs: None,
|
||||||
|
max_tool_iterations: None,
|
||||||
|
hide_cwd_in_prompt: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PiLabToolReceipt {
|
pub struct PiLabToolReceipt {
|
||||||
@@ -291,6 +317,19 @@ pub struct PiLabRpcCommandRequest {
|
|||||||
pub timeout_ms: Option<u64>,
|
pub timeout_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PiLabDiagnosticsRequest {
|
||||||
|
pub command: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub params: Value,
|
||||||
|
#[serde(default)]
|
||||||
|
pub args: Vec<String>,
|
||||||
|
pub timeout_ms: Option<u64>,
|
||||||
|
pub root_uri: Option<String>,
|
||||||
|
pub workspace_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PiLabRenameSessionRequest {
|
pub struct PiLabRenameSessionRequest {
|
||||||
@@ -884,6 +923,7 @@ fn session_from_request(
|
|||||||
runtime_policy_snapshot =
|
runtime_policy_snapshot =
|
||||||
refresh_runtime_policy_permission_mode_value(runtime_policy_snapshot, permission_mode);
|
refresh_runtime_policy_permission_mode_value(runtime_policy_snapshot, permission_mode);
|
||||||
}
|
}
|
||||||
|
runtime_policy_snapshot = refresh_runtime_policy_advanced_config_value(runtime_policy_snapshot);
|
||||||
let now = now_ms();
|
let now = now_ms();
|
||||||
Ok(PiLabSession {
|
Ok(PiLabSession {
|
||||||
session_id: session_id.clone(),
|
session_id: session_id.clone(),
|
||||||
@@ -1032,6 +1072,7 @@ fn pi_lab_start_response(session: &PiLabSession, reused: bool) -> Value {
|
|||||||
"piExtensionToolNames": pi_extension_tool_names,
|
"piExtensionToolNames": pi_extension_tool_names,
|
||||||
"thinkingLevel": session.thinking_level,
|
"thinkingLevel": session.thinking_level,
|
||||||
"permissionMode": session_permission_mode(session),
|
"permissionMode": session_permission_mode(session),
|
||||||
|
"advancedRuntime": pi_lab_effective_advanced_runtime_config(session),
|
||||||
"runtimeImplementation": runtime_impl,
|
"runtimeImplementation": runtime_impl,
|
||||||
"runtimeBinary": runtime_binary,
|
"runtimeBinary": runtime_binary,
|
||||||
"runtimeAvailable": runtime_available,
|
"runtimeAvailable": runtime_available,
|
||||||
@@ -1040,6 +1081,164 @@ fn pi_lab_start_response(session: &PiLabSession, reused: bool) -> Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn policy_string_value(policy: Option<&Value>, keys: &[&str]) -> Option<String> {
|
||||||
|
keys.iter()
|
||||||
|
.find_map(|key| policy.and_then(|value| value.get(*key)))
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn policy_bool_value(policy: Option<&Value>, keys: &[&str]) -> Option<bool> {
|
||||||
|
keys.iter()
|
||||||
|
.find_map(|key| policy.and_then(|value| value.get(*key)))
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn policy_u64_value(policy: Option<&Value>, keys: &[&str]) -> Option<u64> {
|
||||||
|
keys.iter()
|
||||||
|
.find_map(|key| policy.and_then(|value| value.get(*key)))
|
||||||
|
.and_then(|value| {
|
||||||
|
value.as_u64().or_else(|| {
|
||||||
|
value
|
||||||
|
.as_str()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.and_then(|value| value.parse::<u64>().ok())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truthy_config_value(value: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
value.trim().to_ascii_lowercase().as_str(),
|
||||||
|
"1" | "true" | "yes" | "on"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_one_of(value: &str, allowed: &[&str]) -> Option<String> {
|
||||||
|
let normalized = value.trim().to_ascii_lowercase();
|
||||||
|
allowed
|
||||||
|
.iter()
|
||||||
|
.any(|allowed| *allowed == normalized)
|
||||||
|
.then_some(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pi_lab_advanced_runtime_policy(session: &PiLabSession) -> Option<&Value> {
|
||||||
|
session.runtime_policy_snapshot.as_ref().and_then(|policy| {
|
||||||
|
policy
|
||||||
|
.get("piRuntime")
|
||||||
|
.or_else(|| policy.get("advancedRuntime"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pi_lab_effective_advanced_runtime_config(session: &PiLabSession) -> PiLabAdvancedRuntimeConfig {
|
||||||
|
let policy = pi_lab_advanced_runtime_policy(session);
|
||||||
|
let extension_policy = policy_string_value(policy, &["extensionPolicy", "extension_policy"])
|
||||||
|
.or_else(|| env_trimmed("MNOTE_PAGE_AI_PI_EXTENSION_POLICY"))
|
||||||
|
.and_then(|value| normalize_one_of(&value, &["safe", "balanced", "permissive", "standard"]))
|
||||||
|
.map(|value| {
|
||||||
|
if value == "standard" {
|
||||||
|
"balanced".to_string()
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let repair_policy = policy_string_value(policy, &["repairPolicy", "repair_policy"])
|
||||||
|
.or_else(|| env_trimmed("MNOTE_PAGE_AI_PI_REPAIR_POLICY"))
|
||||||
|
.and_then(|value| {
|
||||||
|
normalize_one_of(&value, &["off", "suggest", "auto-safe", "auto-strict"])
|
||||||
|
});
|
||||||
|
let session_durability =
|
||||||
|
policy_string_value(policy, &["sessionDurability", "session_durability"])
|
||||||
|
.or_else(|| env_trimmed("MNOTE_PAGE_AI_PI_SESSION_DURABILITY"))
|
||||||
|
.and_then(|value| normalize_one_of(&value, &["strict", "balanced", "throughput"]));
|
||||||
|
let request_timeout_secs =
|
||||||
|
policy_u64_value(policy, &["requestTimeoutSecs", "request_timeout_secs"])
|
||||||
|
.or_else(|| {
|
||||||
|
env_trimmed("MNOTE_PAGE_AI_PI_REQUEST_TIMEOUT_SECS")
|
||||||
|
.and_then(|value| value.parse::<u64>().ok())
|
||||||
|
})
|
||||||
|
.filter(|value| *value <= 86_400);
|
||||||
|
let max_tool_iterations =
|
||||||
|
policy_u64_value(policy, &["maxToolIterations", "max_tool_iterations"])
|
||||||
|
.or_else(|| {
|
||||||
|
env_trimmed("MNOTE_PAGE_AI_PI_MAX_TOOL_ITERATIONS")
|
||||||
|
.and_then(|value| value.parse::<u64>().ok())
|
||||||
|
})
|
||||||
|
.filter(|value| (1..=1000).contains(value));
|
||||||
|
let hide_cwd_in_prompt = policy_bool_value(policy, &["hideCwdInPrompt", "hide_cwd_in_prompt"])
|
||||||
|
.or_else(|| {
|
||||||
|
env_trimmed("MNOTE_PAGE_AI_PI_HIDE_CWD_IN_PROMPT")
|
||||||
|
.map(|value| truthy_config_value(&value))
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
PiLabAdvancedRuntimeConfig {
|
||||||
|
extension_policy,
|
||||||
|
repair_policy,
|
||||||
|
session_durability,
|
||||||
|
request_timeout_secs,
|
||||||
|
max_tool_iterations,
|
||||||
|
hide_cwd_in_prompt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_runtime_policy_advanced_config_value(mut policy: Value) -> Value {
|
||||||
|
let session = PiLabSession {
|
||||||
|
session_id: String::new(),
|
||||||
|
mnote_user_id: String::new(),
|
||||||
|
bridge_token: String::new(),
|
||||||
|
status: PiLabSessionStatus::Idle,
|
||||||
|
provider_session_id: String::new(),
|
||||||
|
pi_session_dir: String::new(),
|
||||||
|
pi_session_file: None,
|
||||||
|
root_uri: None,
|
||||||
|
workspace_id: None,
|
||||||
|
page_path: None,
|
||||||
|
page_title: None,
|
||||||
|
model_provider: None,
|
||||||
|
model_id: None,
|
||||||
|
thinking_level: None,
|
||||||
|
allowed_roots_snapshot: None,
|
||||||
|
runtime_policy_snapshot: Some(policy.clone()),
|
||||||
|
runtime_pid: None,
|
||||||
|
runtime_mode: String::new(),
|
||||||
|
runtime_error: None,
|
||||||
|
created_at_ms: 0,
|
||||||
|
updated_at_ms: 0,
|
||||||
|
message_count: 0,
|
||||||
|
};
|
||||||
|
let config = pi_lab_effective_advanced_runtime_config(&session);
|
||||||
|
policy["piRuntime"] = serde_json::to_value(config).unwrap_or_else(|_| json!({}));
|
||||||
|
policy
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_pi_lab_advanced_runtime_cli_args(
|
||||||
|
command: &mut Command,
|
||||||
|
config: &PiLabAdvancedRuntimeConfig,
|
||||||
|
) {
|
||||||
|
if let Some(value) = config.extension_policy.as_deref() {
|
||||||
|
command.arg("--extension-policy").arg(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = config.repair_policy.as_deref() {
|
||||||
|
command.arg("--repair-policy").arg(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = config.session_durability.as_deref() {
|
||||||
|
command.arg("--session-durability").arg(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = config.request_timeout_secs {
|
||||||
|
command.arg("--request-timeout").arg(value.to_string());
|
||||||
|
}
|
||||||
|
if let Some(value) = config.max_tool_iterations {
|
||||||
|
command.arg("--max-tool-iterations").arg(value.to_string());
|
||||||
|
}
|
||||||
|
if config.hide_cwd_in_prompt {
|
||||||
|
command.arg("--hide-cwd-in-prompt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn pi_mcp_extension_source() -> Option<String> {
|
fn pi_mcp_extension_source() -> Option<String> {
|
||||||
let value = env_trimmed("MNOTE_PAGE_AI_PI_MCP_EXTENSION")?;
|
let value = env_trimmed("MNOTE_PAGE_AI_PI_MCP_EXTENSION")?;
|
||||||
let normalized = value.to_ascii_lowercase();
|
let normalized = value.to_ascii_lowercase();
|
||||||
@@ -3143,6 +3342,7 @@ async fn start_runtime_for_session(
|
|||||||
)?;
|
)?;
|
||||||
let bridge_base_url = pi_lab_public_base_url();
|
let bridge_base_url = pi_lab_public_base_url();
|
||||||
let pi_extension_tool_names = pi_lab_runtime_pi_extension_tool_names(&session);
|
let pi_extension_tool_names = pi_lab_runtime_pi_extension_tool_names(&session);
|
||||||
|
let advanced_runtime_config = pi_lab_effective_advanced_runtime_config(&session);
|
||||||
let mut pi_tool_names = pi_lab_extension_tool_names(&session);
|
let mut pi_tool_names = pi_lab_extension_tool_names(&session);
|
||||||
pi_tool_names.extend(pi_extension_tool_names.clone());
|
pi_tool_names.extend(pi_extension_tool_names.clone());
|
||||||
pi_tool_names.extend(enabled_builtin_tools.clone());
|
pi_tool_names.extend(enabled_builtin_tools.clone());
|
||||||
@@ -3174,6 +3374,7 @@ async fn start_runtime_for_session(
|
|||||||
"mcpConfigPath": mcp_config_path.clone(),
|
"mcpConfigPath": mcp_config_path.clone(),
|
||||||
"sharedMcpCachePath": shared_mcp_cache_path.clone(),
|
"sharedMcpCachePath": shared_mcp_cache_path.clone(),
|
||||||
"permissionConfigPath": permission_config_path.clone(),
|
"permissionConfigPath": permission_config_path.clone(),
|
||||||
|
"advancedRuntime": advanced_runtime_config.clone(),
|
||||||
"configuredPiExtensionSources": configured_extension_sources.clone(),
|
"configuredPiExtensionSources": configured_extension_sources.clone(),
|
||||||
"piExtensionSources": pi_extension_sources.clone(),
|
"piExtensionSources": pi_extension_sources.clone(),
|
||||||
"piExtensionToolNames": pi_extension_tool_names.clone(),
|
"piExtensionToolNames": pi_extension_tool_names.clone(),
|
||||||
@@ -3196,6 +3397,7 @@ async fn start_runtime_for_session(
|
|||||||
"mcpConfigPath": mcp_config_path,
|
"mcpConfigPath": mcp_config_path,
|
||||||
"sharedMcpCachePath": shared_mcp_cache_path,
|
"sharedMcpCachePath": shared_mcp_cache_path,
|
||||||
"permissionConfigPath": permission_config_path,
|
"permissionConfigPath": permission_config_path,
|
||||||
|
"advancedRuntime": advanced_runtime_config,
|
||||||
"configuredPiExtensionSources": configured_extension_sources,
|
"configuredPiExtensionSources": configured_extension_sources,
|
||||||
"piExtensionSources": pi_extension_sources,
|
"piExtensionSources": pi_extension_sources,
|
||||||
"piExtensionToolNames": pi_extension_tool_names,
|
"piExtensionToolNames": pi_extension_tool_names,
|
||||||
@@ -3248,6 +3450,7 @@ async fn start_runtime_for_session(
|
|||||||
for arg in pi_lab_skill_cli_args(&session) {
|
for arg in pi_lab_skill_cli_args(&session) {
|
||||||
command.arg(arg);
|
command.arg(arg);
|
||||||
}
|
}
|
||||||
|
append_pi_lab_advanced_runtime_cli_args(&mut command, &advanced_runtime_config);
|
||||||
if let Some(provider) = session
|
if let Some(provider) = session
|
||||||
.model_provider
|
.model_provider
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -3391,6 +3594,7 @@ async fn start_runtime_for_session(
|
|||||||
"mnotePiExtension": mnote_pi_extension_path,
|
"mnotePiExtension": mnote_pi_extension_path,
|
||||||
"mcpConfigPath": mcp_config_path,
|
"mcpConfigPath": mcp_config_path,
|
||||||
"permissionConfigPath": permission_config_path,
|
"permissionConfigPath": permission_config_path,
|
||||||
|
"advancedRuntime": advanced_runtime_config,
|
||||||
"configuredPiExtensionSources": configured_extension_sources,
|
"configuredPiExtensionSources": configured_extension_sources,
|
||||||
"piExtensionSources": pi_extension_sources,
|
"piExtensionSources": pi_extension_sources,
|
||||||
"piExtensionToolNames": pi_extension_tool_names,
|
"piExtensionToolNames": pi_extension_tool_names,
|
||||||
@@ -3751,6 +3955,262 @@ fn build_pi_rpc_command(command_type: &str, params: Value) -> Result<Value, WebE
|
|||||||
Ok(command)
|
Ok(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn diagnostics_string_param(params: &Value, keys: &[&str]) -> Option<String> {
|
||||||
|
keys.iter()
|
||||||
|
.find_map(|key| params.get(*key))
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnostics_u64_param(params: &Value, keys: &[&str]) -> Option<u64> {
|
||||||
|
keys.iter()
|
||||||
|
.find_map(|key| params.get(*key))
|
||||||
|
.and_then(|value| {
|
||||||
|
value.as_u64().or_else(|| {
|
||||||
|
value
|
||||||
|
.as_str()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.and_then(|value| value.parse::<u64>().ok())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnostics_string_array_param(params: &Value, keys: &[&str]) -> Vec<String> {
|
||||||
|
keys.iter()
|
||||||
|
.find_map(|key| params.get(*key))
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_diagnostics_output(bytes: &[u8]) -> (String, bool) {
|
||||||
|
let truncated = bytes.len() > PI_LAB_DIAGNOSTICS_MAX_OUTPUT_BYTES;
|
||||||
|
let output = if truncated {
|
||||||
|
&bytes[..PI_LAB_DIAGNOSTICS_MAX_OUTPUT_BYTES]
|
||||||
|
} else {
|
||||||
|
bytes
|
||||||
|
};
|
||||||
|
(String::from_utf8_lossy(output).to_string(), truncated)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn safe_relative_or_absolute_path(value: &str) -> Option<String> {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() || trimmed.contains('\0') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let path = Path::new(trimmed);
|
||||||
|
if path
|
||||||
|
.components()
|
||||||
|
.any(|component| matches!(component, std::path::Component::ParentDir))
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(trimmed.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_pi_diagnostics_args(
|
||||||
|
request: &PiLabDiagnosticsRequest,
|
||||||
|
) -> Result<(Vec<String>, bool), WebError> {
|
||||||
|
if !request.args.is_empty() {
|
||||||
|
return Err(WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_diagnostics_raw_args_not_allowed",
|
||||||
|
"Pi diagnostics 不接受裸 args 透传;请使用 params 白名单字段",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let command = request.command.trim().to_ascii_lowercase();
|
||||||
|
let params = &request.params;
|
||||||
|
let mut args = Vec::new();
|
||||||
|
let mut mutates_cache = false;
|
||||||
|
match command.as_str() {
|
||||||
|
"doctor" => {
|
||||||
|
args.push("doctor".to_string());
|
||||||
|
let format =
|
||||||
|
diagnostics_string_param(params, &["format"]).unwrap_or_else(|| "json".into());
|
||||||
|
let normalized_format = normalize_one_of(&format, &["text", "json", "markdown"])
|
||||||
|
.ok_or_else(|| {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_invalid_diagnostics_format",
|
||||||
|
"doctor format 只能是 text/json/markdown",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
args.push("--format".into());
|
||||||
|
args.push(normalized_format);
|
||||||
|
if let Some(policy) =
|
||||||
|
diagnostics_string_param(params, &["policy", "extensionPolicy", "extension_policy"])
|
||||||
|
{
|
||||||
|
let policy =
|
||||||
|
normalize_one_of(&policy, &["safe", "balanced", "permissive", "standard"])
|
||||||
|
.ok_or_else(|| {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_invalid_diagnostics_policy",
|
||||||
|
"doctor policy 只能是 safe/balanced/permissive/standard",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
args.push("--policy".into());
|
||||||
|
args.push(policy);
|
||||||
|
}
|
||||||
|
if let Some(only) = diagnostics_string_param(params, &["only"]) {
|
||||||
|
let only = normalize_one_of(
|
||||||
|
&only,
|
||||||
|
&[
|
||||||
|
"config",
|
||||||
|
"dirs",
|
||||||
|
"auth",
|
||||||
|
"shell",
|
||||||
|
"sessions",
|
||||||
|
"swarm",
|
||||||
|
"extensions",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_invalid_diagnostics_only",
|
||||||
|
"doctor only 只能是 config/dirs/auth/shell/sessions/swarm/extensions",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
args.push("--only".into());
|
||||||
|
args.push(only);
|
||||||
|
}
|
||||||
|
if let Some(path) =
|
||||||
|
diagnostics_string_param(params, &["path", "extensionPath", "extension_path"])
|
||||||
|
{
|
||||||
|
let path = safe_relative_or_absolute_path(&path).ok_or_else(|| {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_invalid_diagnostics_path",
|
||||||
|
"diagnostics path 不能为空、不能包含 NUL 或 ..",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
args.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"context-preview" | "contextpreview" => {
|
||||||
|
args.push("context-preview".to_string());
|
||||||
|
let format =
|
||||||
|
diagnostics_string_param(params, &["format"]).unwrap_or_else(|| "json".into());
|
||||||
|
let normalized_format =
|
||||||
|
normalize_one_of(&format, &["text", "json"]).ok_or_else(|| {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_invalid_diagnostics_format",
|
||||||
|
"context-preview format 只能是 text/json",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
args.push("--format".into());
|
||||||
|
args.push(normalized_format);
|
||||||
|
if let Some(bead) = diagnostics_string_param(params, &["bead"]) {
|
||||||
|
args.push("--bead".into());
|
||||||
|
args.push(bead);
|
||||||
|
}
|
||||||
|
for changed_path in
|
||||||
|
diagnostics_string_array_param(params, &["changedPaths", "changed_paths"])
|
||||||
|
{
|
||||||
|
let changed_path =
|
||||||
|
safe_relative_or_absolute_path(&changed_path).ok_or_else(|| {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_invalid_diagnostics_path",
|
||||||
|
"changedPath 不能为空、不能包含 NUL 或 ..",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
args.push("--changed-path".into());
|
||||||
|
args.push(changed_path);
|
||||||
|
}
|
||||||
|
if let Some(failing_command) =
|
||||||
|
diagnostics_string_param(params, &["failingCommand", "failing_command"])
|
||||||
|
{
|
||||||
|
args.push("--failing-command".into());
|
||||||
|
args.push(failing_command);
|
||||||
|
}
|
||||||
|
if let Some(max_items) = diagnostics_u64_param(params, &["maxItems", "max_items"]) {
|
||||||
|
if !(1..=100).contains(&max_items) {
|
||||||
|
return Err(WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_invalid_diagnostics_limit",
|
||||||
|
"context-preview maxItems 必须在 1..=100",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
args.push("--max-items".into());
|
||||||
|
args.push(max_items.to_string());
|
||||||
|
}
|
||||||
|
if let Some(max_bytes) = diagnostics_u64_param(params, &["maxBytes", "max_bytes"]) {
|
||||||
|
if !(1024..=1_048_576).contains(&max_bytes) {
|
||||||
|
return Err(WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_invalid_diagnostics_limit",
|
||||||
|
"context-preview maxBytes 必须在 1024..=1048576",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
args.push("--max-bytes".into());
|
||||||
|
args.push(max_bytes.to_string());
|
||||||
|
}
|
||||||
|
let query = diagnostics_string_array_param(params, &["query", "queries"]);
|
||||||
|
args.extend(query);
|
||||||
|
}
|
||||||
|
"list" => {
|
||||||
|
args.push("list".to_string());
|
||||||
|
}
|
||||||
|
"info" => {
|
||||||
|
args.push("info".to_string());
|
||||||
|
let name = diagnostics_string_param(params, &["name", "id"]).ok_or_else(|| {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_diagnostics_name_required",
|
||||||
|
"info 需要 name 或 id",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
args.push(name);
|
||||||
|
}
|
||||||
|
"search" => {
|
||||||
|
args.push("search".to_string());
|
||||||
|
let query = diagnostics_string_param(params, &["query", "q"]).ok_or_else(|| {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_diagnostics_query_required",
|
||||||
|
"search 需要 query",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if let Some(tag) = diagnostics_string_param(params, &["tag"]) {
|
||||||
|
args.push("--tag".into());
|
||||||
|
args.push(tag);
|
||||||
|
}
|
||||||
|
if let Some(sort) = diagnostics_string_param(params, &["sort"]) {
|
||||||
|
let sort = normalize_one_of(&sort, &["relevance", "name"]).ok_or_else(|| {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_invalid_diagnostics_sort",
|
||||||
|
"search sort 只能是 relevance/name",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
args.push("--sort".into());
|
||||||
|
args.push(sort);
|
||||||
|
}
|
||||||
|
if let Some(limit) = diagnostics_u64_param(params, &["limit"]) {
|
||||||
|
if !(1..=100).contains(&limit) {
|
||||||
|
return Err(WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_invalid_diagnostics_limit",
|
||||||
|
"search limit 必须在 1..=100",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
args.push("--limit".into());
|
||||||
|
args.push(limit.to_string());
|
||||||
|
}
|
||||||
|
args.push(query);
|
||||||
|
}
|
||||||
|
"update-index" | "updateindex" => {
|
||||||
|
args.push("update-index".to_string());
|
||||||
|
mutates_cache = true;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(WebError::bad_request_code(
|
||||||
|
"page_ai_pi_lab_diagnostics_command_not_allowed",
|
||||||
|
"Pi diagnostics 只允许 doctor/context-preview/list/info/search/update-index",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((args, mutates_cache))
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_text_operations(current: &str, operations: &Value) -> Result<String, WebError> {
|
fn apply_text_operations(current: &str, operations: &Value) -> Result<String, WebError> {
|
||||||
match operations {
|
match operations {
|
||||||
Value::Array(ops) => {
|
Value::Array(ops) => {
|
||||||
@@ -5034,6 +5494,7 @@ pub async fn status(
|
|||||||
"defaultModelId": default_model_id(),
|
"defaultModelId": default_model_id(),
|
||||||
"defaultThinkingLevel": default_thinking_level(),
|
"defaultThinkingLevel": default_thinking_level(),
|
||||||
"permissionMode": current_session.as_ref().and_then(|session| session_permission_mode(session)),
|
"permissionMode": current_session.as_ref().and_then(|session| session_permission_mode(session)),
|
||||||
|
"advancedRuntime": current_session.as_ref().map(pi_lab_effective_advanced_runtime_config).unwrap_or_else(PiLabAdvancedRuntimeConfig::empty),
|
||||||
"omnirouteBaseUrl": omniroute_base_url(),
|
"omnirouteBaseUrl": omniroute_base_url(),
|
||||||
"piExtensions": current_session
|
"piExtensions": current_session
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -5613,6 +6074,82 @@ pub async fn rpc_command(
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn diagnostics(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(context): Extension<RequestContext>,
|
||||||
|
Json(request): Json<PiLabDiagnosticsRequest>,
|
||||||
|
) -> Result<Json<Value>, WebError> {
|
||||||
|
ensure_enabled(&state)?;
|
||||||
|
cleanup_expired_sessions();
|
||||||
|
ensure_authenticated(&state, &context)?;
|
||||||
|
let (args, mutates_cache) = build_pi_diagnostics_args(&request)?;
|
||||||
|
let binary = pi_binary();
|
||||||
|
if !pi_runtime_binary_available(&binary) {
|
||||||
|
let runtime_impl = pi_runtime_impl();
|
||||||
|
let message = pi_runtime_install_hint(&runtime_impl, &binary)
|
||||||
|
.unwrap_or_else(|| format!("Pi runtime 不可用: {binary}"));
|
||||||
|
return Err(
|
||||||
|
WebError::bad_gateway_code("page_ai_pi_lab_runtime_missing", message).with_details(
|
||||||
|
json!({
|
||||||
|
"runtimeImplementation": runtime_impl,
|
||||||
|
"runtimeBinary": binary,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let cwd = active_allowed_roots(&state, &context)?
|
||||||
|
.into_iter()
|
||||||
|
.find(|root| {
|
||||||
|
request
|
||||||
|
.root_uri
|
||||||
|
.as_deref()
|
||||||
|
.is_none_or(|root_uri| root.root_uri.as_deref() == Some(root_uri))
|
||||||
|
&& request
|
||||||
|
.workspace_id
|
||||||
|
.as_deref()
|
||||||
|
.is_none_or(|workspace_id| root.workspace_id.as_deref() == Some(workspace_id))
|
||||||
|
})
|
||||||
|
.map(|root| root.root_path)
|
||||||
|
.or_else(|| std::env::current_dir().ok());
|
||||||
|
let timeout_ms = request.timeout_ms.unwrap_or(15_000).clamp(1_000, 120_000);
|
||||||
|
let mut command = Command::new(&binary);
|
||||||
|
command
|
||||||
|
.args(&args)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.kill_on_drop(true);
|
||||||
|
if let Some(cwd) = cwd.as_deref() {
|
||||||
|
command.current_dir(cwd);
|
||||||
|
}
|
||||||
|
let output = tokio::time::timeout(Duration::from_millis(timeout_ms), command.output())
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
WebError::new(
|
||||||
|
StatusCode::GATEWAY_TIMEOUT,
|
||||||
|
"page_ai_pi_lab_diagnostics_timeout",
|
||||||
|
format!("Pi diagnostics 超时: {}", request.command.trim()),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.map_err(|error| WebError::internal(format!("执行 Pi diagnostics 失败: {error}")))?;
|
||||||
|
let (stdout, stdout_truncated) = truncate_diagnostics_output(&output.stdout);
|
||||||
|
let (stderr, stderr_truncated) = truncate_diagnostics_output(&output.stderr);
|
||||||
|
Ok(Json(json!({
|
||||||
|
"ok": output.status.success(),
|
||||||
|
"schema": PI_LAB_SCHEMA_DIAGNOSTICS,
|
||||||
|
"command": args.first().cloned().unwrap_or_else(|| request.command.trim().to_string()),
|
||||||
|
"args": args,
|
||||||
|
"mutatesCache": mutates_cache,
|
||||||
|
"runtimeBinary": binary,
|
||||||
|
"cwd": cwd.map(|path| path.to_string_lossy().to_string()),
|
||||||
|
"statusCode": output.status.code(),
|
||||||
|
"stdout": stdout,
|
||||||
|
"stderr": stderr,
|
||||||
|
"stdoutTruncated": stdout_truncated,
|
||||||
|
"stderrTruncated": stderr_truncated,
|
||||||
|
"timeoutMs": timeout_ms,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
/// POST /api/page-ai/pi/state
|
/// POST /api/page-ai/pi/state
|
||||||
/// 封装官方 Pi RPC `get_state`。
|
/// 封装官方 Pi RPC `get_state`。
|
||||||
/// mock 返回完整假数据;real 发送 get_state RPC 并等待响应,超时降级。
|
/// mock 返回完整假数据;real 发送 get_state RPC 并等待响应,超时降级。
|
||||||
@@ -8941,6 +9478,96 @@ mod tests {
|
|||||||
assert!(!source.contains(".arg(\"--no-context-files\")"));
|
assert!(!source.contains(".arg(\"--no-context-files\")"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn advanced_runtime_config_accepts_policy_and_env_fallbacks() {
|
||||||
|
std::env::set_var("MNOTE_PAGE_AI_PI_REPAIR_POLICY", "auto-safe");
|
||||||
|
let root = temp_root("mnote-pi-advanced-runtime");
|
||||||
|
let mut session = permission_mode_test_session(&root, "full_access");
|
||||||
|
session.runtime_policy_snapshot = Some(json!({
|
||||||
|
"piRuntime": {
|
||||||
|
"extensionPolicy": "permissive",
|
||||||
|
"sessionDurability": "throughput",
|
||||||
|
"requestTimeoutSecs": 120,
|
||||||
|
"maxToolIterations": 80,
|
||||||
|
"hideCwdInPrompt": true
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let config = pi_lab_effective_advanced_runtime_config(&session);
|
||||||
|
assert_eq!(config.extension_policy.as_deref(), Some("permissive"));
|
||||||
|
assert_eq!(config.repair_policy.as_deref(), Some("auto-safe"));
|
||||||
|
assert_eq!(config.session_durability.as_deref(), Some("throughput"));
|
||||||
|
assert_eq!(config.request_timeout_secs, Some(120));
|
||||||
|
assert_eq!(config.max_tool_iterations, Some(80));
|
||||||
|
assert!(config.hide_cwd_in_prompt);
|
||||||
|
std::env::remove_var("MNOTE_PAGE_AI_PI_REPAIR_POLICY");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diagnostics_builder_allows_only_safe_official_commands() {
|
||||||
|
let doctor = PiLabDiagnosticsRequest {
|
||||||
|
command: "doctor".into(),
|
||||||
|
params: json!({"only": "sessions", "policy": "balanced"}),
|
||||||
|
args: Vec::new(),
|
||||||
|
timeout_ms: None,
|
||||||
|
root_uri: None,
|
||||||
|
workspace_id: None,
|
||||||
|
};
|
||||||
|
let (args, mutates_cache) = build_pi_diagnostics_args(&doctor).expect("doctor args");
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec!["doctor", "--format", "json", "--policy", "balanced", "--only", "sessions"]
|
||||||
|
);
|
||||||
|
assert!(!mutates_cache);
|
||||||
|
|
||||||
|
let search = PiLabDiagnosticsRequest {
|
||||||
|
command: "search".into(),
|
||||||
|
params: json!({"query": "git", "sort": "name", "limit": 10}),
|
||||||
|
args: Vec::new(),
|
||||||
|
timeout_ms: None,
|
||||||
|
root_uri: None,
|
||||||
|
workspace_id: None,
|
||||||
|
};
|
||||||
|
let (args, _) = build_pi_diagnostics_args(&search).expect("search args");
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec!["search", "--sort", "name", "--limit", "10", "git"]
|
||||||
|
);
|
||||||
|
|
||||||
|
let update_index = PiLabDiagnosticsRequest {
|
||||||
|
command: "updateIndex".into(),
|
||||||
|
params: json!({}),
|
||||||
|
args: Vec::new(),
|
||||||
|
timeout_ms: None,
|
||||||
|
root_uri: None,
|
||||||
|
workspace_id: None,
|
||||||
|
};
|
||||||
|
let (args, mutates_cache) =
|
||||||
|
build_pi_diagnostics_args(&update_index).expect("update-index args");
|
||||||
|
assert_eq!(args, vec!["update-index"]);
|
||||||
|
assert!(mutates_cache);
|
||||||
|
|
||||||
|
let denied = PiLabDiagnosticsRequest {
|
||||||
|
command: "install".into(),
|
||||||
|
params: json!({"source": "npm:unsafe"}),
|
||||||
|
args: Vec::new(),
|
||||||
|
timeout_ms: None,
|
||||||
|
root_uri: None,
|
||||||
|
workspace_id: None,
|
||||||
|
};
|
||||||
|
assert!(build_pi_diagnostics_args(&denied).is_err());
|
||||||
|
|
||||||
|
let raw_args_denied = PiLabDiagnosticsRequest {
|
||||||
|
command: "doctor".into(),
|
||||||
|
params: json!({}),
|
||||||
|
args: vec!["--fix".into()],
|
||||||
|
timeout_ms: None,
|
||||||
|
root_uri: None,
|
||||||
|
workspace_id: None,
|
||||||
|
};
|
||||||
|
assert!(build_pi_diagnostics_args(&raw_args_denied).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_sessions_filters_by_current_user_and_pi_profile() {
|
async fn list_sessions_filters_by_current_user_and_pi_profile() {
|
||||||
let app = test_app();
|
let app = test_app();
|
||||||
|
|||||||
@@ -206,6 +206,11 @@ const checks = [
|
|||||||
['route starts Pi with MNote tool allowlist', route.includes('--tools') && route.includes('mnoteToolNames') && route.includes('pi_lab_extension_tool_names')],
|
['route starts Pi with MNote tool allowlist', route.includes('--tools') && route.includes('mnoteToolNames') && route.includes('pi_lab_extension_tool_names')],
|
||||||
['route keeps Pi Rust official prompt templates and @file context enabled', !route.includes('.arg("--no-prompt-templates")') && !route.includes('.arg("--no-context-files")')],
|
['route keeps Pi Rust official prompt templates and @file context enabled', !route.includes('.arg("--no-prompt-templates")') && !route.includes('.arg("--no-context-files")')],
|
||||||
['route exposes controlled Pi Rust RPC command wrapper', route.includes('PI_LAB_SCHEMA_RPC_COMMAND') && route.includes('build_pi_rpc_command') && route.includes('abort_bash') && route.includes('get_available_models')],
|
['route exposes controlled Pi Rust RPC command wrapper', route.includes('PI_LAB_SCHEMA_RPC_COMMAND') && route.includes('build_pi_rpc_command') && route.includes('abort_bash') && route.includes('get_available_models')],
|
||||||
|
['route exposes controlled Pi Rust diagnostics wrapper', route.includes('PI_LAB_SCHEMA_DIAGNOSTICS') && route.includes('pub async fn diagnostics') && route.includes('build_pi_diagnostics_args') && routesMod.includes('/api/page-ai/pi/diagnostics')],
|
||||||
|
['route only allows read-only or cache-only Pi diagnostics commands', route.includes('doctor/context-preview/list/info/search/update-index') && route.includes('mutatesCache') && route.includes('page_ai_pi_lab_diagnostics_command_not_allowed')],
|
||||||
|
['route rejects raw Pi diagnostics args passthrough', route.includes('page_ai_pi_lab_diagnostics_raw_args_not_allowed') && route.includes('不接受裸 args 透传')],
|
||||||
|
['route passes official Pi Rust advanced runtime flags', route.includes('--extension-policy') && route.includes('--repair-policy') && route.includes('--session-durability') && route.includes('--request-timeout') && route.includes('--max-tool-iterations') && route.includes('--hide-cwd-in-prompt')],
|
||||||
|
['route exposes effective Pi advanced runtime config', route.includes('advancedRuntime') && route.includes('pi_lab_effective_advanced_runtime_config') && route.includes('MNOTE_PAGE_AI_PI_EXTENSION_POLICY') && route.includes('MNOTE_PAGE_AI_PI_REPAIR_POLICY')],
|
||||||
['route maps mid-stream send to official steer/follow_up commands', route.includes('Some("steer") => "steer"') && route.includes('Some("follow-up") | Some("followUp") | Some("follow_up") => "follow_up"')],
|
['route maps mid-stream send to official steer/follow_up commands', route.includes('Some("steer") => "steer"') && route.includes('Some("follow-up") | Some("followUp") | Some("follow_up") => "follow_up"')],
|
||||||
['route sends Pi RPC images payload without MNote file substitution', route.includes('pub images: Option<Vec<Value>>') && route.includes('command["images"] = json!(images)')],
|
['route sends Pi RPC images payload without MNote file substitution', route.includes('pub images: Option<Vec<Value>>') && route.includes('command["images"] = json!(images)')],
|
||||||
['route abort uses official RPC without killing runtime process', route.includes('json!({"type": "abort"})') && !route.includes('let _ = kill_session_process(&request.session_id).await;')],
|
['route abort uses official RPC without killing runtime process', route.includes('json!({"type": "abort"})') && !route.includes('let _ = kill_session_process(&request.session_id).await;')],
|
||||||
@@ -258,6 +263,7 @@ const checks = [
|
|||||||
['mod.rs mounts pi start route', routesMod.includes('/api/page-ai/pi/start')],
|
['mod.rs mounts pi start route', routesMod.includes('/api/page-ai/pi/start')],
|
||||||
['mod.rs mounts pi send route', routesMod.includes('/api/page-ai/pi/send')],
|
['mod.rs mounts pi send route', routesMod.includes('/api/page-ai/pi/send')],
|
||||||
['mod.rs mounts pi abort route', routesMod.includes('/api/page-ai/pi/abort')],
|
['mod.rs mounts pi abort route', routesMod.includes('/api/page-ai/pi/abort')],
|
||||||
|
['mod.rs mounts pi diagnostics route', routesMod.includes('/api/page-ai/pi/diagnostics')],
|
||||||
['mod.rs mounts pi events route', routesMod.includes('/api/page-ai/pi/events')],
|
['mod.rs mounts pi events route', routesMod.includes('/api/page-ai/pi/events')],
|
||||||
['mod.rs mounts pi session history delete and clear routes', routesMod.includes('get(page_ai_pi::list_sessions).delete(page_ai_pi::clear_sessions)') && routesMod.includes('.delete(page_ai_pi::delete_session_history)')],
|
['mod.rs mounts pi session history delete and clear routes', routesMod.includes('get(page_ai_pi::list_sessions).delete(page_ai_pi::clear_sessions)') && routesMod.includes('.delete(page_ai_pi::delete_session_history)')],
|
||||||
['mod.rs mounts pi tool-call route', routesMod.includes('/api/page-ai/pi/tool-call')],
|
['mod.rs mounts pi tool-call route', routesMod.includes('/api/page-ai/pi/tool-call')],
|
||||||
|
|||||||
Reference in New Issue
Block a user