chore: add code run entry check
This commit is contained in:
@@ -39,3 +39,4 @@ Streaming runtime operations:
|
||||
node scripts/runtime-worker-drain.mjs drain goosed-3
|
||||
node scripts/runtime-worker-drain.mjs undrain goosed-3
|
||||
node scripts/check-tool-runtime.mjs
|
||||
node scripts/check-agent-code-run-entry.mjs
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/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://mm.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 = {
|
||||
enabledEnv: process.env.VITE_AGENT_CODE_RUNS_ENABLED ?? null,
|
||||
autodetectEnv: process.env.VITE_AGENT_CODE_RUNS_AUTODETECT ?? 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 &&
|
||||
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);
|
||||
@@ -279,6 +279,10 @@ async function writeMetadata() {
|
||||
path.join(root, 'scripts', 'check-tool-runtime.mjs'),
|
||||
path.join(runtimeRoot, 'scripts', 'check-tool-runtime.mjs'),
|
||||
);
|
||||
await fs.copyFile(
|
||||
path.join(root, 'scripts', 'check-agent-code-run-entry.mjs'),
|
||||
path.join(runtimeRoot, 'scripts', 'check-agent-code-run-entry.mjs'),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runtimeRoot, 'RUNBOOK.txt'),
|
||||
[
|
||||
@@ -323,6 +327,7 @@ async function writeMetadata() {
|
||||
' node scripts/runtime-worker-drain.mjs drain goosed-3',
|
||||
' node scripts/runtime-worker-drain.mjs undrain goosed-3',
|
||||
' node scripts/check-tool-runtime.mjs',
|
||||
' node scripts/check-agent-code-run-entry.mjs',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/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://mm.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 = {
|
||||
enabledEnv: process.env.VITE_AGENT_CODE_RUNS_ENABLED ?? null,
|
||||
autodetectEnv: process.env.VITE_AGENT_CODE_RUNS_AUTODETECT ?? 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 &&
|
||||
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);
|
||||
Reference in New Issue
Block a user