14a00774d9
- 新增 web 能力并挂载 platform/web(web_search/fetch_url) - 实时查询强制 web skill,router fallback 与 await session Finish - Session Broker 覆盖率/指标、stream replay 与相关单测/E2E 脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
170 lines
5.2 KiB
JavaScript
170 lines
5.2 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
/**
|
|
* SESSION BROKER COVERAGE (H5 Session architecture Patch 2 / §5.7)
|
|
*
|
|
* Business code must not call userAuth session ownership helpers directly.
|
|
* Use session-broker.mjs facade via createSessionAccess / resolveSessionAccess.
|
|
*
|
|
* See docs/h5-session-architecture-20260706.md §5.7
|
|
*/
|
|
|
|
/** @typedef {{ file: string, line: number, rule: string, text: string }} SessionBrokerCoverageViolation */
|
|
|
|
export const SESSION_BROKER_FACADE_FILE = 'session-broker.mjs';
|
|
export const SESSION_BROKER_DEFINITION_FILE = 'user-auth.mjs';
|
|
|
|
/** Files allowed to call userAuth.registerAgentSession / ownsSession directly. */
|
|
export const SESSION_BROKER_DIRECT_ALLOWLIST = new Set([
|
|
SESSION_BROKER_FACADE_FILE,
|
|
SESSION_BROKER_DEFINITION_FILE,
|
|
]);
|
|
|
|
/** Patch 2 modules that must route session ownership through sessionAccess. */
|
|
export const SESSION_BROKER_PATCH2_FILES = [
|
|
'tkmind-proxy.mjs',
|
|
'agent-run-routes.mjs',
|
|
'agent-run-gateway.mjs',
|
|
'direct-chat-service.mjs',
|
|
'server.mjs',
|
|
'wechat-mp.mjs',
|
|
'mindspace-page-edit-session.mjs',
|
|
'mindspace-agent-runner.mjs',
|
|
'mindspace-conversation-package-routes.mjs',
|
|
];
|
|
|
|
export const SESSION_BROKER_FORBIDDEN_CALLS = [
|
|
{ id: 'register-agent-session', pattern: /\buserAuth\.registerAgentSession\s*\(/ },
|
|
{ id: 'owns-session', pattern: /\buserAuth\.ownsSession\s*\(/ },
|
|
{ id: 'get-session-target', pattern: /\buserAuth\.getSessionTarget\s*\(/ },
|
|
{ id: 'unregister-agent-session', pattern: /\buserAuth\.unregisterAgentSession\s*\(/ },
|
|
];
|
|
|
|
export const SESSION_BROKER_PATCH2_MARKERS = [
|
|
/\bresolveSessionAccess\b/,
|
|
/\bcreateSessionAccess\b/,
|
|
/\bsessionAccess\b/,
|
|
];
|
|
|
|
export const SESSION_BROKER_SCAN_IGNORE = [
|
|
/^scripts\//,
|
|
/^mindspace-service\//,
|
|
/^\.runtime\//,
|
|
/^node_modules\//,
|
|
/\.test\.mjs$/,
|
|
/\.test\.ts$/,
|
|
];
|
|
|
|
export const SESSION_BROKER_LINE_ALLOW = [
|
|
/SESSION BROKER COVERAGE/,
|
|
/§5\.7/,
|
|
/session-broker\.mjs/,
|
|
/createSessionAccess/,
|
|
/resolveSessionAccess/,
|
|
/sessionAccess\./,
|
|
/getSessionAccess/,
|
|
];
|
|
|
|
function relativePosix(rootDir, absolutePath) {
|
|
return path.relative(rootDir, absolutePath).split(path.sep).join('/');
|
|
}
|
|
|
|
function isIgnoredScanPath(relativePath) {
|
|
return SESSION_BROKER_SCAN_IGNORE.some((pattern) => pattern.test(relativePath));
|
|
}
|
|
|
|
function isTestFile(relativePath) {
|
|
return /\.test\.mjs$/.test(relativePath);
|
|
}
|
|
|
|
function isLineAllowed(line) {
|
|
return SESSION_BROKER_LINE_ALLOW.some((pattern) => pattern.test(line));
|
|
}
|
|
|
|
function scanDirectUserAuthCalls(relativePath, content) {
|
|
/** @type {SessionBrokerCoverageViolation[]} */
|
|
const violations = [];
|
|
if (SESSION_BROKER_DIRECT_ALLOWLIST.has(relativePath) || isTestFile(relativePath)) {
|
|
return violations;
|
|
}
|
|
|
|
const lines = content.split('\n');
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
const line = lines[index];
|
|
if (!line.trim() || line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
|
|
if (isLineAllowed(line)) continue;
|
|
for (const rule of SESSION_BROKER_FORBIDDEN_CALLS) {
|
|
if (!rule.pattern.test(line)) continue;
|
|
violations.push({
|
|
file: relativePath,
|
|
line: index + 1,
|
|
rule: rule.id,
|
|
text: line.trim(),
|
|
});
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
function checkPatch2ModuleWiring(rootDir) {
|
|
/** @type {SessionBrokerCoverageViolation[]} */
|
|
const violations = [];
|
|
for (const relativePath of SESSION_BROKER_PATCH2_FILES) {
|
|
const absolutePath = path.join(rootDir, relativePath);
|
|
if (!fs.existsSync(absolutePath)) {
|
|
violations.push({
|
|
file: relativePath,
|
|
line: 0,
|
|
rule: 'missing-patch2-file',
|
|
text: 'Patch 2 module missing from repository',
|
|
});
|
|
continue;
|
|
}
|
|
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
if (!SESSION_BROKER_PATCH2_MARKERS.some((pattern) => pattern.test(content))) {
|
|
violations.push({
|
|
file: relativePath,
|
|
line: 0,
|
|
rule: 'missing-session-access-wiring',
|
|
text: 'Expected resolveSessionAccess/createSessionAccess/sessionAccess wiring',
|
|
});
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
export function checkSessionBrokerCoverage(rootDir, { scanRepo = true } = {}) {
|
|
/** @type {SessionBrokerCoverageViolation[]} */
|
|
const violations = [...checkPatch2ModuleWiring(rootDir)];
|
|
|
|
if (!scanRepo) return violations;
|
|
|
|
const queue = [rootDir];
|
|
while (queue.length > 0) {
|
|
const current = queue.pop();
|
|
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
const absolutePath = path.join(current, entry.name);
|
|
const relativePath = relativePosix(rootDir, absolutePath);
|
|
if (entry.isDirectory()) {
|
|
queue.push(absolutePath);
|
|
continue;
|
|
}
|
|
if (!entry.name.endsWith('.mjs')) continue;
|
|
if (isIgnoredScanPath(relativePath)) continue;
|
|
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
violations.push(...scanDirectUserAuthCalls(relativePath, content));
|
|
}
|
|
}
|
|
|
|
return violations;
|
|
}
|
|
|
|
export function formatSessionBrokerCoverageViolations(violations) {
|
|
return violations
|
|
.map((item) => `${item.file}:${item.line} [${item.rule}] ${item.text}`)
|
|
.join('\n');
|
|
}
|