#!/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( "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: OpenHub / Pi Lab 独立入口未删除 // -------------------------------------------------------------------------- console.log("\n-- 6. OpenHub / Pi Lab 独立入口未删除 --"); 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" ); // -------------------------------------------------------------------------- // 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") && aiSettingsRs.includes("pub mcp_servers: Vec") && 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('"sk-') && !pageAiPiRs.includes('"secret') && !pageAiPiRs.includes('"API_KEY'), "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);