Files
mnote/scripts/task-ai-management-control-plane-static-smoke.js
Agent Board b798f628ee chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align
chevron/DOM with restored expansion; keep Sidex-style shallow page-tree
scan and drop the unused recursive scanner that only added cargo noise.

Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi
into a module package, and retire Hermes/ACP/OpenHub recycle + root
harness evidence from the index while gitignoring recycle and local
diag dumps.

Archive superseded design/bugs docs under old/, point architecture at
ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor
regressions so the working tree can stay clean.
2026-07-21 05:13:05 +08:00

556 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 主入口与退役边界
// --------------------------------------------------------------------------
console.log("\n-- 6. Pi Rust 主入口与退役边界 --");
check(
"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"
);
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(
"AI management no longer exposes OpenHub admin entry",
!aiAdminRs.includes("/page-ai/openhub") &&
!aiAdminRs.includes("打开 OpenHub Admin") &&
!aiAdminRs.includes("OpenHub 仍是默认 Page AI"),
"OpenHub admin links removed from active AI management UI"
);
// --------------------------------------------------------------------------
// 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 优先的管理员鉴权"
);
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);