#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { Agent, fetch as undiciFetch } from 'undici'; import mysql from 'mysql2/promise'; function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) return; for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const idx = trimmed.indexOf('='); if (idx < 0) continue; const key = trimmed.slice(0, idx).trim(); const value = trimmed.slice(idx + 1).trim(); if (!process.env[key]) process.env[key] = value; } } function parseMysqlConfig() { if (process.env.DATABASE_URL) { const url = new URL(process.env.DATABASE_URL); if (url.protocol !== 'mysql:') { throw new Error(`Unsupported DATABASE_URL scheme for code-run entry check: ${url.protocol}`); } return { host: url.hostname, port: Number(url.port || 3306), user: decodeURIComponent(url.username), password: decodeURIComponent(url.password), database: url.pathname.replace(/^\/+/, ''), charset: 'utf8mb4', }; } return { host: process.env.MYSQL_HOST, port: Number(process.env.MYSQL_PORT || 3306), user: process.env.MYSQL_USER, password: process.env.MYSQL_PASSWORD, database: process.env.MYSQL_DATABASE, charset: 'utf8mb4', }; } function truthy(value) { return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase()); } function listJsAssets(distRoot) { const assetsDir = path.join(distRoot, 'assets'); if (!fs.existsSync(assetsDir)) return []; return fs.readdirSync(assetsDir) .filter((name) => name.endsWith('.js')) .map((name) => path.join(assetsDir, name)); } function checkFrontendBundle(distRoot) { const jsAssets = listJsAssets(distRoot); const text = jsAssets.map((file) => fs.readFileSync(file, 'utf8')).join('\n'); return { distRoot, jsAssetCount: jsAssets.length, hasAgentRunEndpoint: text.includes('/agent/runs'), hasToolModePayload: text.includes('tool_mode'), hasCodeLiteral: text.includes('"code"') || text.includes("'code'") || text.includes(':code'), note: 'VITE_AGENT_CODE_RUNS_* are build-time flags; runtime checks can prove code entry is bundled, not whether a past build enabled the flag.', }; } async function queryToolGrants() { const conn = await mysql.createConnection(parseMysqlConfig()); try { const [roleRows] = await conn.query( `SELECT capability_key, allowed FROM h5_capability_grants WHERE subject_type = 'role' AND subject_id = 'user' AND capability_key IN ('aider', 'openhands') ORDER BY capability_key`, ); const roleDefaults = Object.fromEntries( roleRows.map((row) => [row.capability_key, Boolean(row.allowed)]), ); const [userRows] = await conn.query( `SELECT subject_id, capability_key, allowed FROM h5_capability_grants WHERE subject_type = 'user' AND capability_key IN ('aider', 'openhands') AND allowed = 1 ORDER BY subject_id, capability_key`, ); const grantsByUser = new Map(); for (const row of userRows) { if (!grantsByUser.has(row.subject_id)) grantsByUser.set(row.subject_id, []); grantsByUser.get(row.subject_id).push(row.capability_key); } return { roleDefaults, whitelistUserCount: grantsByUser.size, sampledUsers: [...grantsByUser.entries()].slice(0, 10).map(([userId, grantedTools]) => ({ userId, grantedTools: grantedTools.sort(), chatHasCodeTools: false, codeHasCodeTools: grantedTools.includes('aider') || grantedTools.includes('openhands'), })), }; } finally { await conn.end(); } } const envFile = process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env'); loadEnvFile(envFile); const distRoot = path.resolve(process.env.MEMIND_H5_DIST_ROOT || path.join(process.cwd(), 'dist')); const publicBase = (process.env.H5_PUBLIC_BASE_URL || 'https://m.tkmind.cn').replace(/\/$/, ''); const insecureDispatcher = new Agent({ connect: { rejectUnauthorized: false } }); async function fetchJson(pathname) { const res = await undiciFetch(`${publicBase}${pathname}`, { dispatcher: publicBase.startsWith('https://127.0.0.1') ? insecureDispatcher : undefined, }); const text = await res.text(); let json = null; try { json = JSON.parse(text); } catch { json = null; } return { ok: res.ok, status: res.status, json, text: json ? undefined : text.slice(0, 160) }; } const frontend = checkFrontendBundle(distRoot); const runtime = await fetchJson('/api/runtime/status').catch((err) => ({ ok: false, error: err.message })); const toolGrants = await queryToolGrants().catch((err) => ({ ok: false, error: err instanceof Error ? (err.message || err.code || err.name) : String(err), })); const buildFlags = { serverCodeRunsEnabledEnv: process.env.MEMIND_AGENT_CODE_RUNS_ENABLED ?? null, serverCodeRunsEnabledTruthy: truthy(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED), enabledEnv: process.env.VITE_AGENT_CODE_RUNS_ENABLED ?? null, autodetectEnv: process.env.VITE_AGENT_CODE_RUNS_AUTODETECT ?? null, enabledUserIdsEnv: process.env.VITE_AGENT_CODE_RUNS_USER_IDS ?? null, serverCodeRunUserIdsEnv: process.env.MEMIND_AGENT_CODE_RUNS_USER_IDS ?? null, enabledTruthy: truthy(process.env.VITE_AGENT_CODE_RUNS_ENABLED), autodetectTruthy: truthy(process.env.VITE_AGENT_CODE_RUNS_AUTODETECT), note: 'These env values describe the current shell, not necessarily the already-built dist.', }; const runtimePolicy = runtime.json?.toolRuntime ?? {}; const roleDefaults = toolGrants.roleDefaults ?? {}; const ok = Boolean( frontend.jsAssetCount > 0 && frontend.hasAgentRunEndpoint && frontend.hasToolModePayload && runtime.ok && runtimePolicy.defaultMode === 'chat' && runtimePolicy.codeToolMode === 'code' && runtimePolicy.chatInjectsCodeTools === false && runtimePolicy.codeRunsEnabled === buildFlags.serverCodeRunsEnabledTruthy && roleDefaults.aider === false && roleDefaults.openhands === false && (toolGrants.sampledUsers ?? []).every((user) => user.chatHasCodeTools === false) && (toolGrants.sampledUsers ?? []).every((user) => user.codeHasCodeTools === true) ); console.log(JSON.stringify({ ok, checkedAt: new Date().toISOString(), publicBase, envFile, frontend, buildFlags, runtimePolicy, toolGrants, writes: { database: false, mindSpace: false, createsAgentRun: false, }, }, null, 2)); process.exit(ok ? 0 : 1);