567 lines
21 KiB
JavaScript
567 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
||
"use strict";
|
||
|
||
// --------------------------------------------------------------------------
|
||
// 7-71 统一 AI 管理面板与 Pi Lab 功能接入 — 静态代码核查
|
||
// 脚本只读,不启动服务器。逐项检查代码结构中的路由注册、数据源逻辑、
|
||
// 安全约束、账户菜单入口和 Pi Lab persistence 存在性。
|
||
// --------------------------------------------------------------------------
|
||
|
||
const assert = require("node:assert");
|
||
const fs = require("node:fs");
|
||
const path = require("node:path");
|
||
|
||
const REPO_ROOT = "/mnt/Data1T/mnote";
|
||
|
||
const PASS = "\x1b[32m✓\x1b[0m";
|
||
const FAIL = "\x1b[31m✗\x1b[0m";
|
||
const SKIP = "\x1b[33m–\x1b[0m";
|
||
|
||
let passed = 0;
|
||
let failed = 0;
|
||
let skipped = 0;
|
||
|
||
function check(name, ok, detail) {
|
||
if (ok) {
|
||
console.log(` ${PASS} ${name}`);
|
||
if (detail) console.log(` ${detail}`);
|
||
passed++;
|
||
} else {
|
||
console.log(` ${FAIL} ${name}`);
|
||
if (detail) console.log(` ${detail}`);
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
function checkSkipped(name, detail) {
|
||
console.log(` ${SKIP} ${name}`);
|
||
if (detail) console.log(` ${detail}`);
|
||
skipped++;
|
||
}
|
||
|
||
function readFile(p) {
|
||
const full = path.join(REPO_ROOT, p);
|
||
return fs.readFileSync(full, "utf8");
|
||
}
|
||
|
||
function fileExists(p) {
|
||
const full = path.join(REPO_ROOT, p);
|
||
return fs.existsSync(full);
|
||
}
|
||
|
||
function countMatches(text, pattern) {
|
||
const matches = text.match(pattern);
|
||
return matches ? matches.length : 0;
|
||
}
|
||
|
||
function routeHandlerInLines(lines, route, handler) {
|
||
return lines.some((l, i) => {
|
||
if (!l.includes(route)) return false;
|
||
if (l.includes(handler)) return true;
|
||
const nextLine = lines[i + 1] || "";
|
||
return nextLine.includes(handler);
|
||
});
|
||
}
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Section 1: Routes Registration
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n-- 1. Routes 注册 --");
|
||
|
||
const routesMod = readFile("rust/crates/mnote-web/src/routes/mod.rs");
|
||
const routesModLines = routesMod.split("\n");
|
||
|
||
check(
|
||
"/api/ai-settings/effective registered as GET",
|
||
routeHandlerInLines(routesModLines, '/api/ai-settings/effective"', 'get(ai_settings::effective_settings)'),
|
||
'Found get(ai_settings::effective_settings)'
|
||
);
|
||
|
||
check(
|
||
"/api/ai-settings/access-scopes registered as GET",
|
||
routeHandlerInLines(routesModLines, '/api/ai-settings/access-scopes"', 'get(ai_settings::user_access_scopes)'),
|
||
'Found get(ai_settings::user_access_scopes)'
|
||
);
|
||
|
||
check(
|
||
"/api/ai-admin/access-scopes registered as GET",
|
||
routeHandlerInLines(routesModLines, '/api/ai-admin/access-scopes"', 'get(ai_settings::admin_access_scopes)'),
|
||
'Found get(ai_settings::admin_access_scopes)'
|
||
);
|
||
|
||
check(
|
||
"/api/ai-settings/receipts registered as GET",
|
||
routeHandlerInLines(routesModLines, '/api/ai-settings/receipts"', 'get(ai_settings::user_receipts)'),
|
||
'Found get(ai_settings::user_receipts)'
|
||
);
|
||
|
||
check(
|
||
"/api/ai-admin/receipts registered as GET",
|
||
routeHandlerInLines(routesModLines, '/api/ai-admin/receipts"', 'get(ai_settings::admin_receipts)'),
|
||
'Found get(ai_settings::admin_receipts)'
|
||
);
|
||
|
||
check(
|
||
"/admin/ai registered",
|
||
routesMod.includes('/admin/ai", get(gateway::admin_ai_entry)'),
|
||
'gateway::admin_ai_entry'
|
||
);
|
||
|
||
check(
|
||
"/user/ai registered",
|
||
routesMod.includes('/user/ai", get(gateway::user_ai_entry)'),
|
||
'gateway::user_ai_entry'
|
||
);
|
||
|
||
const aiSettingsAccessScopesPostPutDelete = routesModLines.filter(function(l, i) {
|
||
var isAccessScopeRoute = l.includes('/api/ai-settings/access-scopes"') || l.includes('/api/ai-admin/access-scopes"');
|
||
if (!isAccessScopeRoute) return false;
|
||
var nextLine = routesModLines[i + 1] || "";
|
||
return nextLine.includes("post(") || nextLine.includes("put(") || nextLine.includes("delete(");
|
||
});
|
||
check(
|
||
"AI access-scopes routes have no POST/PUT/DELETE",
|
||
aiSettingsAccessScopesPostPutDelete.length === 0,
|
||
"All ai-settings/ai-admin access-scopes endpoints are GET-only"
|
||
);
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Section 2: effective 数据仅从 directory_grants 生成
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n-- 2. effective 代码 source of truth --");
|
||
|
||
var aiSettingsRs = readFile("rust/crates/mnote-web/src/routes/ai_settings.rs");
|
||
|
||
check(
|
||
"effective_settings uses load_active_directory_grants",
|
||
aiSettingsRs.includes("load_active_directory_grants(&state, &actor_id"),
|
||
"Uses directory_grants, not allowed_roots_json"
|
||
);
|
||
|
||
check(
|
||
"load_model_policy_and_quota never reads allowed_roots_json",
|
||
aiSettingsRs.includes("Never reads `allowed_roots_json`") &&
|
||
aiSettingsRs.includes("model_policy_json") &&
|
||
aiSettingsRs.includes("quota_json") &&
|
||
!aiSettingsRs.includes("allowed_roots_json,") &&
|
||
!aiSettingsRs.includes('"allowed_roots_json"'),
|
||
"Only reads model_policy_json and quota_json"
|
||
);
|
||
|
||
check(
|
||
"Response declares source_of_truth = 'directory_grants'",
|
||
countMatches(aiSettingsRs, 'SOURCE_OF_TRUTH') >= 1 &&
|
||
aiSettingsRs.includes('const SOURCE_OF_TRUTH: &str = "directory_grants"'),
|
||
"SOURCE_OF_TRUTH constant = directory_grants"
|
||
);
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Section 3: AI 页面无 access scope POST/PUT/DELETE
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n-- 3. AI 管理页面无 access-scope 写操作 --");
|
||
|
||
var aiAdminRs = readFile("rust/crates/mnote-web/src/ssr/pages/ai_admin.rs");
|
||
|
||
check(
|
||
"AI admin script only uses GET for access-scopes",
|
||
!aiAdminRs.includes("access-scopes').*POST") &&
|
||
!aiAdminRs.includes("access-scopes').*PUT") &&
|
||
!aiAdminRs.includes("access-scopes').*DELETE") &&
|
||
!aiAdminRs.includes("access-scopes\\\\', { method: 'POST'") &&
|
||
!aiAdminRs.includes("access-scopes\\\\', { method: 'PUT'") &&
|
||
!aiAdminRs.includes("access-scopes\\\\', { method: 'DELETE'"),
|
||
"Access-scopes fetch uses implicit GET; no POST/PUT/DELETE in script"
|
||
);
|
||
|
||
check(
|
||
"SSR template has no access-scopes write forms/buttons",
|
||
!aiAdminRs.includes('data-admin-form="create-share-grant"') &&
|
||
!aiAdminRs.includes('data-admin-action="revoke-access-grant"') &&
|
||
!aiAdminRs.includes('name="rootPath"') &&
|
||
!aiAdminRs.includes('name="targetUserId"'),
|
||
"No write form elements present"
|
||
);
|
||
|
||
check(
|
||
"requestJson calls for access-scopes don't supply POST/PUT/DELETE method",
|
||
!aiAdminRs.includes("requestJson('/api/ai-admin/access-scopes', { method: 'POST'") &&
|
||
!aiAdminRs.includes("requestJson('/api/ai-admin/access-scopes', { method: 'PUT'") &&
|
||
!aiAdminRs.includes("requestJson('/api/ai-settings/access-scopes', { method: 'POST'") &&
|
||
!aiAdminRs.includes("requestJson('/api/ai-settings/access-scopes', { method: 'PUT'"),
|
||
"No explicit POST/PUT/DELETE in fetch calls for access-scopes"
|
||
);
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Section 4: 账户菜单有独立 AI 管理入口
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n-- 4. 账户菜单 AI 管理入口 --");
|
||
|
||
var sidebarWorkspaceJs = readFile("rust/crates/mnote-web/browser/sidebar-workspace-runtime.js");
|
||
|
||
check(
|
||
"Account menu has 'AI 管理' entry with mnote-account-ai-management testid",
|
||
sidebarWorkspaceJs.includes('data-testid="mnote-account-ai-management"') &&
|
||
sidebarWorkspaceJs.includes("AI 管理"),
|
||
"Found mnote-account-ai-management element with AI 管理 text"
|
||
);
|
||
|
||
check(
|
||
"Account menu no longer exposes retired 授权管理 entry",
|
||
!sidebarWorkspaceJs.includes('data-testid="mnote-account-access-policy"') &&
|
||
!sidebarWorkspaceJs.includes("openAdminAccessPolicyDialog"),
|
||
"Retired access-policy modal entry is absent from account menu runtime"
|
||
);
|
||
|
||
check(
|
||
"AI management entry navigates to /admin/ai or /user/ai",
|
||
sidebarWorkspaceJs.includes("'/admin/ai'") &&
|
||
sidebarWorkspaceJs.includes("'/user/ai'") &&
|
||
sidebarWorkspaceJs.includes("sessionIsAdmin(session) ? '/admin/ai' : '/user/ai'"),
|
||
"Navigates to /admin/ai for admin, /user/ai for user"
|
||
);
|
||
|
||
check(
|
||
"AI management entry has hidden attribute initially",
|
||
sidebarWorkspaceJs.includes('aiManagementLink.hidden = false') &&
|
||
sidebarWorkspaceJs.includes('data-ai-management-role'),
|
||
"Hidden initially; shown after session fetch"
|
||
);
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Section 5: Pi history routes 与 journal calls
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n-- 5. Pi Lab history routes & journal persistence --");
|
||
|
||
check(
|
||
"Pi Lab /api/page-ai/pi/sessions GET route exists",
|
||
routeHandlerInLines(routesModLines, '/api/page-ai/pi/sessions"', "get(page_ai_pi::list_sessions)"),
|
||
"list_sessions handler exists"
|
||
);
|
||
|
||
check(
|
||
"Pi Lab /api/page-ai/pi/sessions/{session_id} GET route exists",
|
||
routeHandlerInLines(routesModLines, '/api/page-ai/pi/sessions/{session_id}"', "get(page_ai_pi::get_session_history)"),
|
||
"get_session_history handler exists"
|
||
);
|
||
|
||
check(
|
||
"Pi Lab /api/page-ai/pi/sessions/{session_id}/events GET route exists",
|
||
routeHandlerInLines(routesModLines, '/api/page-ai/pi/sessions/{session_id}/events"', "get(page_ai_pi::get_session_events)"),
|
||
"get_session_events handler exists"
|
||
);
|
||
|
||
var pageAiPiRs = readFile("rust/crates/mnote-web/src/routes/page_ai_pi.rs");
|
||
|
||
check(
|
||
"Pi Lab persist_upsert_run writes to control_plane ai_runtime_runs",
|
||
pageAiPiRs.includes("persist_upsert_run") &&
|
||
pageAiPiRs.includes(".upsert_ai_runtime_run(input)") &&
|
||
pageAiPiRs.includes("build_upsert_run_input"),
|
||
"Uses control_plane().upsert_ai_runtime_run()"
|
||
);
|
||
|
||
check(
|
||
"Pi Lab persist_append_event writes to control_plane ai_runtime_events",
|
||
pageAiPiRs.includes("persist_append_event") &&
|
||
pageAiPiRs.includes(".append_ai_runtime_event(input)") &&
|
||
pageAiPiRs.includes("build_append_event_input"),
|
||
"Uses control_plane().append_ai_runtime_event()"
|
||
);
|
||
|
||
check(
|
||
"Pi Lab session list filters by pi_lab profile and pi acp_runtime",
|
||
pageAiPiRs.includes("r.profile == PI_LAB_PROFILE") &&
|
||
pageAiPiRs.includes("r.acp_runtime == PI_LAB_ACP_RUNTIME") &&
|
||
pageAiPiRs.includes('PI_LAB_PROFILE: &str = "pi_lab"') &&
|
||
pageAiPiRs.includes('PI_LAB_ACP_RUNTIME: &str = "pi"'),
|
||
"list_sessions filters to pi_lab/pi runs only"
|
||
);
|
||
|
||
check(
|
||
"Pi Lab receipts persist to control-plane",
|
||
pageAiPiRs.includes(".append_ai_tool_event(") &&
|
||
pageAiPiRs.includes(".append_ai_file_patch(") &&
|
||
pageAiPiRs.includes('"control_plane_turso_libsql_v1"'),
|
||
"Tool receipts and successful patches use ai_tool_events / ai_file_patches"
|
||
);
|
||
|
||
check(
|
||
"JSONL receipt storage is debug fallback only",
|
||
pageAiPiRs.includes('"provider_neutral_jsonl_debug_fallback_v1"') &&
|
||
pageAiPiRs.includes('"fallbackReason"'),
|
||
"JSONL is retained only after a control-plane write failure"
|
||
);
|
||
|
||
check(
|
||
"AI management page loads real sessions and receipts",
|
||
aiAdminRs.includes("/api/page-ai/pi/sessions?limit=20") &&
|
||
aiAdminRs.includes("/api/ai-admin/receipts?limit=50") &&
|
||
aiAdminRs.includes("data-ai-admin-receipts-body"),
|
||
"Sessions & Receipts is no longer a placeholder"
|
||
);
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Section 6: Pi Rust 主入口与 OpenHub 兼容边界
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n-- 6. Pi Rust 主入口与 OpenHub 兼容边界 --");
|
||
|
||
check(
|
||
"OpenHub agent route /page-ai/openhub/ai exists",
|
||
routesMod.includes('/page-ai/openhub/ai", get(page_ai_openhub::ai_shell)'),
|
||
"page_ai_openhub::ai_shell"
|
||
);
|
||
|
||
check(
|
||
"OpenHub admin routes still exist",
|
||
routesMod.includes('/page-ai/openhub/admin"') &&
|
||
routesMod.includes("page_ai_openhub::non_ai_route_guard"),
|
||
"non_ai_route_guard for admin"
|
||
);
|
||
|
||
check(
|
||
"OpenHub status API still exists",
|
||
routesMod.includes('/api/page-ai/openhub/status", get(page_ai_openhub::status)'),
|
||
"GET page_ai_openhub::status"
|
||
);
|
||
|
||
check(
|
||
"Pi Lab shell route exists",
|
||
routesMod.includes('/page-ai/pi", get(page_ai_pi::shell)'),
|
||
"page_ai_pi::shell"
|
||
);
|
||
|
||
check(
|
||
"Pi Lab event stream exists",
|
||
routesMod.includes('/api/page-ai/pi/events", get(page_ai_pi::events)'),
|
||
"GET page_ai_pi::events"
|
||
);
|
||
|
||
check(
|
||
"Pi Lab start/send/abort routes exist",
|
||
routesMod.includes('/api/page-ai/pi/start", post(page_ai_pi::start)') &&
|
||
routesMod.includes('/api/page-ai/pi/send", post(page_ai_pi::send)') &&
|
||
routesMod.includes('/api/page-ai/pi/abort", post(page_ai_pi::abort)'),
|
||
"POST start, send, abort"
|
||
);
|
||
|
||
check(
|
||
"AI management service panel treats Pi Rust as default Page AI runtime",
|
||
aiAdminRs.includes("Pi Rust 是默认 Page AI runtime") &&
|
||
aiAdminRs.includes("Pi Rust Page AI") &&
|
||
aiAdminRs.includes("runtimeImplementation") &&
|
||
aiAdminRs.includes("runtimeBinary") &&
|
||
aiAdminRs.includes("runtimeAvailable") &&
|
||
aiAdminRs.includes("runtimeError"),
|
||
"Pi Rust service panel exposes runtime implementation, binary, availability, and error"
|
||
);
|
||
|
||
check(
|
||
"AI management service panel exposes effective Pi advanced runtime",
|
||
aiAdminRs.includes("Advanced runtime") &&
|
||
aiAdminRs.includes("advancedRuntimeSummary") &&
|
||
aiAdminRs.includes("data-ai-admin-pi-advanced-runtime-json") &&
|
||
aiAdminRs.includes("extensionPolicy") &&
|
||
aiAdminRs.includes("maxToolIterations"),
|
||
"Shows effective extension/repair/session/request/tool-iteration policy instead of hiding host decisions"
|
||
);
|
||
|
||
check(
|
||
"AI management service panel exposes controlled Pi official diagnostics",
|
||
routesMod.includes('/api/page-ai/pi/diagnostics", post(page_ai_pi::diagnostics)') &&
|
||
aiAdminRs.includes('data-ai-admin-pi-diagnostic-command="doctor"') &&
|
||
aiAdminRs.includes('data-ai-admin-pi-diagnostic-command="context-preview"') &&
|
||
aiAdminRs.includes('data-ai-admin-pi-diagnostic-command="list"') &&
|
||
aiAdminRs.includes('data-ai-admin-pi-diagnostic-command="update-index"') &&
|
||
aiAdminRs.includes('data-ai-admin-pi-diagnostic-command="search"') &&
|
||
aiAdminRs.includes('data-ai-admin-pi-diagnostic-command="info"') &&
|
||
aiAdminRs.includes("runPiDiagnostic") &&
|
||
aiAdminRs.includes("/api/page-ai/pi/diagnostics"),
|
||
"Doctor/context-preview/list/search/info/update-index are available from Pi service diagnostics"
|
||
);
|
||
|
||
check(
|
||
"OpenHub is described as compatibility/admin boundary, not default chat",
|
||
aiAdminRs.includes("迁移期兼容 / admin 边界") &&
|
||
aiAdminRs.includes("不再作为默认 Page AI chat") &&
|
||
!aiAdminRs.includes("OpenHub 仍是默认 Page AI"),
|
||
"OpenHub remains only as migration compatibility boundary"
|
||
);
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Section 7: Admin 原子策略 GET/PUT
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n-- 7. Admin 原子策略 GET/PUT --");
|
||
|
||
check(
|
||
"/api/ai-admin/settings GET+PUT route registered",
|
||
routesMod.includes('"/api/ai-admin/settings"') &&
|
||
routesMod.includes("get(ai_settings::admin_get_settings).put(ai_settings::admin_put_settings)"),
|
||
"模型、工具、Skills、MCP 由单一原子策略端点管理"
|
||
);
|
||
|
||
check(
|
||
"admin settings requires admin authorization",
|
||
aiSettingsRs.includes("ensure_admin(&context)?") &&
|
||
aiSettingsRs.includes("is_local_access_policy_admin_context"),
|
||
"GET/PUT 都复用现有管理员鉴权"
|
||
);
|
||
|
||
check(
|
||
"admin policy persists through ai_policies model_policy_json",
|
||
aiSettingsRs.includes("UpsertAiPolicyInput") &&
|
||
aiSettingsRs.includes("model_policy_json: merged_model_policy_json") &&
|
||
aiSettingsRs.includes("upsert_ai_policy"),
|
||
"不新增第二套配置真相"
|
||
);
|
||
|
||
check(
|
||
"effective settings projects tools, skills and MCP",
|
||
aiSettingsRs.includes("pub skills: Vec<SkillConfig>") &&
|
||
aiSettingsRs.includes("pub mcp_servers: Vec<McpServerConfig>") &&
|
||
aiSettingsRs.includes("effective_tool_catalog") &&
|
||
aiSettingsRs.includes("effective_skill_registry") &&
|
||
aiSettingsRs.includes("effective_mcp_registry"),
|
||
"用户侧只读取管理员策略的 effective 投影"
|
||
);
|
||
|
||
check(
|
||
"default Skills registry includes requested Pi capability set",
|
||
[
|
||
'"vpn".into()',
|
||
'"chrome-bridge".into()',
|
||
'"context7".into()',
|
||
'"searxng".into()',
|
||
'"global-search".into()',
|
||
'"mempalace".into()',
|
||
'"codegraph".into()',
|
||
].every((needle) => aiSettingsRs.includes(needle)) &&
|
||
aiSettingsRs.includes("default_skill_registry"),
|
||
"vpn/chrome-bridge/context7/searxng/global-search/mempalace/codegraph are installed as default Skills"
|
||
);
|
||
|
||
check(
|
||
"default MCP registry includes requested facade servers",
|
||
aiSettingsRs.includes("default_mcp_server_registry") &&
|
||
aiSettingsRs.includes("node /home/lix/apps/codex-chrome-bridge/mcp/chrome-bridge-mcp.mjs") &&
|
||
aiSettingsRs.includes("https://mcp.context7.com/mcp") &&
|
||
aiSettingsRs.includes("node /home/lix/.agent-infra/searxng/codex-searxng-mcp.cjs") &&
|
||
aiSettingsRs.includes("mempalace.mcp_server") &&
|
||
aiSettingsRs.includes("codegraph serve --mcp"),
|
||
"chrome-bridge/context7/searxng/mempalace/codegraph are installed as facade-only MCP defaults"
|
||
);
|
||
|
||
check(
|
||
"AI admin UI uses the atomic settings endpoint",
|
||
aiAdminRs.includes("/api/ai-admin/settings") &&
|
||
aiAdminRs.includes("method: 'PUT'") &&
|
||
!aiAdminRs.includes("/api/ai-admin/config"),
|
||
"UI 与后端合同一致"
|
||
);
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Section 9: secretRef-only -- 无硬编码 API key
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n-- 9. secretRef-only 检查 --");
|
||
|
||
check(
|
||
"ai_settings.rs accepts secret references only",
|
||
aiSettingsRs.includes('starts_with("env://")') &&
|
||
aiSettingsRs.includes('starts_with("secret://")') &&
|
||
aiSettingsRs.includes("不允许直接传 API Key"),
|
||
"Raw provider credentials are rejected"
|
||
);
|
||
|
||
check(
|
||
"ai_admin.rs refers to secrets only as env references, not hardcoded",
|
||
aiAdminRs.includes("API key 与 secret 不进前端、不硬编码"),
|
||
"ai_admin.rs declares secrets never enter frontend or hardcode"
|
||
);
|
||
|
||
check(
|
||
"page_ai_pi.rs uses env-based omniroute_api_key(), no hardcoded secrets",
|
||
pageAiPiRs.includes("PiLabToolFacade") &&
|
||
pageAiPiRs.includes("omniroute_api_key") &&
|
||
pageAiPiRs.includes("env_trimmed") &&
|
||
pageAiPiRs.includes("MNOTE_PAGE_AI_PI_OMNIROUTE_API_KEY") &&
|
||
!pageAiPiRs.includes('"sk-'),
|
||
"Pi Lab uses env-based key via omniroute_api_key() and env_trimmed"
|
||
);
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Section 10: allowed roots 不可写
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n-- 10. allowed roots 不可写检查 --");
|
||
|
||
check(
|
||
"No route with 'allowed-roots' accepting POST/PUT/DELETE",
|
||
!routesModLines.some(function(l) {
|
||
return (l.includes("allowed-roots") || l.includes("allowed_roots") || l.includes("allowedRoots")) &&
|
||
(l.includes("post(") || l.includes("put(") || l.includes("delete("));
|
||
}),
|
||
"No write-allowed-roots endpoint in routes"
|
||
);
|
||
|
||
check(
|
||
"ai_settings.rs has no write_allowed_roots / update_allowed_roots",
|
||
!aiSettingsRs.includes("write_allowed_roots") &&
|
||
!aiSettingsRs.includes("update_allowed_roots") &&
|
||
!aiSettingsRs.includes("save_allowed_roots"),
|
||
"No function for writing allowed roots in ai_settings.rs"
|
||
);
|
||
|
||
check(
|
||
"ai_admin.rs SSR has no allowed-roots write form elements",
|
||
!aiAdminRs.includes('name="allowedRoots"') &&
|
||
!aiAdminRs.includes('name="allowed_roots"') &&
|
||
!aiAdminRs.includes("data-ai-admin-allowed-roots-edit"),
|
||
"No allowed-roots editable fields in SSR template"
|
||
);
|
||
|
||
check(
|
||
"ai_admin script has no POST/PUT/DELETE for allowed-roots",
|
||
!aiAdminRs.includes("allowed-roots')") &&
|
||
!aiAdminRs.includes("allowed_roots')") &&
|
||
!aiAdminRs.includes("allowedRoots')"),
|
||
"No fetch calls for allowed-roots endpoints in ai_admin script"
|
||
);
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Section 11: MCP facade-only
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n-- 11. MCP facade-only 检查 --");
|
||
|
||
check(
|
||
"Pi Lab tool facade only exposes mnote. tools, no raw MCP passthrough",
|
||
pageAiPiRs.includes("mnote.current_page.read") &&
|
||
pageAiPiRs.includes("mnote.local_file.read") &&
|
||
pageAiPiRs.includes("mnote.knowledge_rag.query") &&
|
||
!pageAiPiRs.includes("tools/mcp") &&
|
||
!pageAiPiRs.includes("use_mcp") &&
|
||
!pageAiPiRs.includes("MCP_SERVER"),
|
||
"PiLabToolFacade registers only mnote. tools; no direct MCP"
|
||
);
|
||
|
||
check(
|
||
"ai_admin.rs MCP section says 'MNote facade' not 'raw MCP'",
|
||
aiAdminRs.includes("MCP 通过 MNote facade 管控") &&
|
||
aiAdminRs.includes("不出现 raw API key") &&
|
||
aiSettingsRs.includes("facadeOnly") &&
|
||
aiSettingsRs.includes("sandbox"),
|
||
"MCP section in ai_admin restricts raw MCP access via facade"
|
||
);
|
||
|
||
check(
|
||
"ai_admin.rs Knowledge section references MNote facade",
|
||
aiAdminRs.includes("MNote knowledge facade"),
|
||
"Pi knowledge goes through MNote facade, not direct"
|
||
);
|
||
|
||
// --------------------------------------------------------------------------
|
||
// 汇总
|
||
// --------------------------------------------------------------------------
|
||
console.log("\n===========================================");
|
||
console.log("通过: " + passed + " 失败: " + failed + " 跳过: " + skipped);
|
||
console.log("===========================================\n");
|
||
|
||
process.exit(failed > 0 ? 1 : 0);
|