Files

556 lines
21 KiB
JavaScript
Raw Permalink Normal View History

2026-07-04 21:47:42 +08:00
#!/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"
);
2026-07-10 10:54:34 +08:00
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"
);
2026-07-04 21:47:42 +08:00
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"
);
// --------------------------------------------------------------------------
2026-07-12 14:18:26 +08:00
// Section 6: Pi Rust 主入口与退役边界
2026-07-04 21:47:42 +08:00
// --------------------------------------------------------------------------
2026-07-12 14:18:26 +08:00
console.log("\n-- 6. Pi Rust 主入口与退役边界 --");
2026-07-04 21:47:42 +08:00
check(
2026-07-12 14:18:26 +08:00
"OpenHub active routes are retired from mnote-web",
!routesMod.includes("page_ai_openhub") &&
!routesMod.includes("/page-ai/openhub") &&
!routesMod.includes("/api/page-ai/openhub"),
"OpenHub route module and public routes removed from active router"
2026-07-04 21:47:42 +08:00
);
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"
);
2026-07-10 10:54:34 +08:00
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"
);
2026-07-12 07:03:26 +08:00
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"
);
2026-07-10 10:54:34 +08:00
check(
2026-07-12 14:18:26 +08:00
"AI management no longer exposes OpenHub admin entry",
!aiAdminRs.includes("/page-ai/openhub") &&
!aiAdminRs.includes("打开 OpenHub Admin") &&
2026-07-10 10:54:34 +08:00
!aiAdminRs.includes("OpenHub 仍是默认 Page AI"),
2026-07-12 14:18:26 +08:00
"OpenHub admin links removed from active AI management UI"
2026-07-10 10:54:34 +08:00
);
2026-07-04 21:47:42 +08:00
// --------------------------------------------------------------------------
// 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(&state, &context)?") &&
aiSettingsRs.includes("current_actor_is_local_admin(state, context)"),
"GET/PUT 都复用 session 优先的管理员鉴权"
2026-07-04 21:47:42 +08:00
);
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") &&
2026-07-10 10:54:34 +08:00
pageAiPiRs.includes("MNOTE_PAGE_AI_PI_OMNIROUTE_API_KEY") &&
!pageAiPiRs.includes('"sk-'),
2026-07-04 21:47:42 +08:00
"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);