import fs from 'node:fs'; import path from 'node:path'; /** * GOOSED PROXY BOUNDARY (H5 Session architecture Patch 5) * * H5 chat 主链路 Portal → goosed 的唯一 adapter 是 tkmind-proxy.mjs。 * 其它模块须通过 tkmindProxy.apiFetch / apiFetchTo 间接访问,不得自建 createApiFetch。 * * 见 docs/h5-session-architecture-20260706.md §7.2 */ /** @typedef {{ file: string, line: number, rule: string, text: string }} GoosedBoundaryViolation */ export const GOOSED_PROXY_ADAPTER_FILE = 'tkmind-proxy.mjs'; export const GOOSED_DIRECT_ALLOWLIST = [ GOOSED_PROXY_ADAPTER_FILE, 'mindspace-agent-runner.mjs', 'mindspace-page-edit-session.mjs', 'wechat-mp.mjs', 'session-reconcile.mjs', ]; export const GOOSED_PROXY_GUARDED_FILES = [ 'server.mjs', 'agent-run-gateway.mjs', 'agent-run-routes.mjs', 'direct-chat-service.mjs', 'chat-intent-router.mjs', ]; export const GOOSED_BOUNDARY_LINE_ALLOW = [ /tkmindProxy\./, /GOOSED PROXY BOUNDARY/, /AGENT_RUNS_REQUIRED/, /runHandlerChain\(tkmindProxy/, /handlers\['POST \/agent/, /api\.(post|get|put|delete|patch)\(['"]\/agent/, /api\.(post|get|put|delete|patch)\(['"]\/sessions/, /req\.path\.endsWith\(['"]\/reply['"]\)/, /req\.path\.endsWith\(['"]\/events['"]\)/, /parseApiTargets/, /TKMIND_API_TARGET/, /createTkmindProxy/, /API_TARGETS/, /proxyFallback/, /proxySessionEvents/, ]; export const GOOSED_BOUNDARY_FORBIDDEN = [ { id: 'create-api-fetch', pattern: /\bcreateApiFetch\s*\(/ }, { id: 'direct-sessions-reply', pattern: /\bapiFetch\s*\(\s*[`'"]\/sessions\/[^`'"]+\/reply/, }, { id: 'direct-sessions-events', pattern: /\bapiFetch\s*\(\s*[`'"]\/sessions\/[^`'"]+\/events/, }, { id: 'direct-agent-start', pattern: /\bapiFetch\s*\(\s*[`'"]\/agent\/start/ }, { id: 'direct-agent-resume', pattern: /\bapiFetch\s*\(\s*[`'"]\/agent\/resume/ }, { id: 'raw-goosed-target-fetch', pattern: /fetch\s*\(\s*[`'"]https?:\/\/127\.0\.0\.1:1800[67]/, }, ]; export const GOOSED_BOUNDARY_SCAN_IGNORE = [ /^scripts\//, /^mindspace-service\//, /^\.runtime\//, /^node_modules\//, /\.test\.mjs$/, /\.test\.ts$/, ]; function relativePosix(rootDir, absolutePath) { return path.relative(rootDir, absolutePath).split(path.sep).join('/'); } function isIgnoredScanPath(relativePath) { return GOOSED_BOUNDARY_SCAN_IGNORE.some((pattern) => pattern.test(relativePath)); } function isLineAllowed(line) { return GOOSED_BOUNDARY_LINE_ALLOW.some((pattern) => pattern.test(line)); } function scanFileContent(relativePath, content, { guardedOnly = false } = {}) { /** @type {GoosedBoundaryViolation[]} */ const 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 GOOSED_BOUNDARY_FORBIDDEN) { if (!rule.pattern.test(line)) continue; violations.push({ file: relativePath, line: index + 1, rule: rule.id, text: line.trim(), }); } } if (guardedOnly && violations.length === 0) return violations; return violations; } export function checkGoosedProxyBoundary(rootDir, { extraAllowlist = [], extraGuarded = [], scanRepo = true, } = {}) { /** @type {GoosedBoundaryViolation[]} */ const violations = []; const allowlist = new Set([...GOOSED_DIRECT_ALLOWLIST, ...extraAllowlist]); const guarded = [...GOOSED_PROXY_GUARDED_FILES, ...extraGuarded]; for (const relativePath of guarded) { const absolutePath = path.join(rootDir, relativePath); if (!fs.existsSync(absolutePath)) { violations.push({ file: relativePath, line: 0, rule: 'missing-guarded-file', text: 'Guarded file missing from repository', }); continue; } const content = fs.readFileSync(absolutePath, 'utf8'); violations.push(...scanFileContent(relativePath, content, { guardedOnly: true })); } 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; if (allowlist.has(relativePath)) continue; if (guarded.includes(relativePath)) continue; const content = fs.readFileSync(absolutePath, 'utf8'); violations.push(...scanFileContent(relativePath, content)); } } return violations; } export function formatGoosedBoundaryViolations(violations) { if (!violations.length) return ''; return violations .map((item) => `${item.file}:${item.line} [${item.rule}] ${item.text}`) .join('\n'); }