Files
memind/src/utils/agentRunMode.ts
T
john 239c41f935 feat: add memindadm runtime policy for agent code runs (Phase 1.5)
Persist code-run gates in admin DB and expose them via /auth/status so H5 can honor runtime policy without VITE rebuilds.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 21:33:27 +08:00

347 lines
13 KiB
TypeScript

import type { MindSpaceChatContext, AgentCodeRunClientPolicy } from '../types';
export type AgentRunCreateOptions = {
toolMode?: 'chat' | 'code';
taskType?: string | null;
forceDeepReasoning?: boolean;
validation?: AgentRunValidation | null;
validationInstruction?: string | null;
selectedAssetIds?: string[];
};
export type AgentRunValidationFile = {
path: string;
contains?: string;
};
export type AgentRunValidation = {
expectedFile?: AgentRunValidationFile | string;
expectedFiles?: Array<AgentRunValidationFile | string>;
};
function envFlag(value: unknown): boolean {
const normalized = String(value ?? '').trim().toLowerCase();
return ['1', 'true', 'yes', 'on'].includes(normalized);
}
export const agentCodeRunsEnabled = envFlag(import.meta.env.VITE_AGENT_CODE_RUNS_ENABLED);
export const agentCodeRunsAutodetectEnabled = envFlag(
import.meta.env.VITE_AGENT_CODE_RUNS_AUTODETECT,
);
export const agentPageDataDevAutodetectEnabled = envFlag(
import.meta.env.VITE_AGENT_PAGE_DATA_DEV_AUTODETECT,
);
let runtimeAgentCodeRunPolicy: AgentCodeRunClientPolicy | null = null;
export function applyAgentCodeRunClientPolicy(policy: AgentCodeRunClientPolicy | null) {
runtimeAgentCodeRunPolicy = policy;
}
function clientCodeRunsEnabled(): boolean {
if (runtimeAgentCodeRunPolicy?.codeRun?.enabled != null) {
return runtimeAgentCodeRunPolicy.codeRun.enabled;
}
return agentCodeRunsEnabled;
}
function clientCodeRunsAutodetectEnabled(): boolean {
if (runtimeAgentCodeRunPolicy?.codeRun?.generalAutodetect != null) {
return runtimeAgentCodeRunPolicy.codeRun.generalAutodetect;
}
return agentCodeRunsAutodetectEnabled;
}
function clientPageDataDevAutodetectEnabled(): boolean {
if (runtimeAgentCodeRunPolicy?.codeRun?.pageDataDevAutodetect != null) {
return runtimeAgentCodeRunPolicy.codeRun.pageDataDevAutodetect;
}
return agentPageDataDevAutodetectEnabled;
}
export const PAGE_DATA_DEV_TASK_TYPE = 'page_data_dev';
function parseUserIdSet(value: unknown): Set<string> {
return new Set(
String(value ?? '')
.split(',')
.map((item) => item.trim())
.filter(Boolean),
);
}
const agentCodeRunUserIds = parseUserIdSet(import.meta.env.VITE_AGENT_CODE_RUNS_USER_IDS);
export function agentCodeRunsEnabledForUser(userId?: string | null): boolean {
if (!clientCodeRunsEnabled()) return false;
if (agentCodeRunUserIds.size === 0) return true;
return Boolean(userId && agentCodeRunUserIds.has(userId));
}
const PAGE_DATA_DEV_TASK_PATTERNS = [
/columns_not_allowed/i,
/\bpage[\s-]?data[\s-]?dev\b/i,
/(?:insert||).{0,16}(?:|403||error)/iu,
/(?:|||debug|fix).{0,24}(?:|page[\s-]?data|||admin||policy||bind)/iu,
/(?:|page[\s-]?data||).{0,24}(?:bug||||)/iu,
/(?:|verify).{0,16}(?:||)/iu,
/(?:repair|).{0,16}(?:bind||policy|)/iu,
];
export function isPageDataDevTaskText(text: string): boolean {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
return PAGE_DATA_DEV_TASK_PATTERNS.some((pattern) => pattern.test(normalized));
}
export function resolvePageDataDevTaskType(text: string): string | null {
if (!clientPageDataDevAutodetectEnabled()) return null;
return isPageDataDevTaskText(text) ? PAGE_DATA_DEV_TASK_TYPE : null;
}
const CODE_TASK_PATTERNS = [
/\b(repo|repository|branch|commit|pull request|pr|diff|patch)\b/i,
/\b(aider|openhands|codex|codebase|workspace)\b/i,
/\b(refactor|debug|fix bug|implement|add test|unit test)\b/i,
/(修改|修复|重构|调试|实现|新增|编写|更新).{0,12}(代码|仓库|分支|文件|测试|组件|接口)/,
/(代码|仓库|项目|文件|组件|接口).{0,12}(修改|修复|重构|调试|实现|新增|编写|更新)/,
];
const DEEP_REASONING_TASK_PATTERNS = [
/\b(public\/[^\s"'<>]+\.html)\b/i,
/\b(html|h5|web\s?page|landing\s?page|microsite|docx|word)\b/i,
/\b(write|create|generate|publish|download|export|save|edit)\b.{0,40}\b(file|page|html|docx|word|asset)\b/i,
/\b(file|page|html|docx|word|asset)\b.{0,40}\b(write|create|generate|publish|download|export|save|edit)\b/i,
/(?:|||||||||||).{0,24}(?:||HTML|html|H5|h5|||Word|word|docx|||||)/u,
/(?:||HTML|html|H5|h5|||Word|word|docx|||||).{0,24}(?:|||||||||||)/u,
/MindSpace\/[^/\s]+\/public\/[^\s"'<>]+\.html/i,
];
function sanitizeRequestIdForPath(requestId: string): string {
const normalized = String(requestId ?? '').trim().replace(/[^a-zA-Z0-9._-]/g, '-');
return normalized || 'unknown-request';
}
function normalizeExpectedFile(value: AgentRunValidationFile | string): AgentRunValidationFile | null {
if (typeof value === 'string') {
const path = value.trim();
return path ? { path } : null;
}
const path = String(value?.path ?? '').trim();
if (!path) return null;
return {
path,
...(value.contains == null ? {} : { contains: String(value.contains) }),
};
}
function mergeAgentRunValidationFiles(
...items: Array<AgentRunValidation | AgentRunValidationFile | string | null | undefined>
): AgentRunValidation | null {
const byKey = new Map<string, AgentRunValidationFile>();
const addFile = (file: AgentRunValidationFile | string | null | undefined) => {
if (!file) return;
const normalized = normalizeExpectedFile(file);
if (!normalized) return;
const key = `${normalized.path}\u0000${normalized.contains ?? ''}`;
byKey.set(key, normalized);
};
for (const item of items) {
if (!item) continue;
if (typeof item === 'string' || 'path' in item) {
addFile(item);
continue;
}
addFile(item.expectedFile);
for (const file of item.expectedFiles ?? []) addFile(file);
}
const expectedFiles = [...byKey.values()];
return expectedFiles.length ? { expectedFiles } : null;
}
export function normalizeAgentRunPublicHtmlPath(candidate: string): string | null {
const decoded = decodeURIComponent(String(candidate ?? '')).replace(/\\/g, '/').trim();
const publicMatch = decoded.match(/(?:^|\/)(public\/[^"'<>?#\s]+\.html)\b/i);
const raw = publicMatch?.[1] ?? '';
if (!raw) return null;
const parts = raw
.split('/')
.map((part) => part.trim())
.filter(Boolean);
if (parts.length < 2 || parts[0] !== 'public') return null;
if (parts.some((part) => part === '.' || part === '..')) return null;
return parts.join('/');
}
export function extractAgentRunPublicHtmlPaths(...values: Array<string | null | undefined>): string[] {
const found = new Set<string>();
const pattern = /(?:^|[\s"'([{<])((?:https?:\/\/[^\s"'<>]+|\/?MindSpace\/[^\s"'<>]+|public\/[^\s"'<>]+)\.html)\b/gi;
for (const value of values) {
const text = String(value ?? '');
let match: RegExpExecArray | null;
while ((match = pattern.exec(text))) {
const path = normalizeAgentRunPublicHtmlPath(match[1] ?? '');
if (path) found.add(path);
}
}
return [...found];
}
export function buildAgentRunTaskValidation({
requestId,
taskType,
text,
mindspaceContext,
pageEdit,
}: {
requestId: string;
taskType: string;
text: string;
mindspaceContext?: MindSpaceChatContext | null;
pageEdit?: { pageId?: string | null; pageTitle?: string | null } | null;
}): { validation: AgentRunValidation | null; instruction: string } {
const safeRequestId = sanitizeRequestIdForPath(requestId);
const pageId = String(pageEdit?.pageId ?? mindspaceContext?.page?.id ?? '').trim();
const pageTitle = String(pageEdit?.pageTitle ?? mindspaceContext?.page?.title ?? '').trim();
const publicHtmlPaths = extractAgentRunPublicHtmlPaths(
text,
mindspaceContext?.page?.publicationUrl ?? null,
);
const expectedFiles: AgentRunValidationFile[] = publicHtmlPaths.map((path) => ({ path }));
const instructions: string[] = [];
if (publicHtmlPaths.length > 0) {
instructions.push(
'',
'[Memind task artifact validation]',
'The task references concrete MindSpace public HTML artifacts.',
'Before finishing, ensure these workspace-relative files exist and contain the intended result:',
...publicHtmlPaths.map((path) => `- ${path}`),
);
}
if (taskType === 'page_edit_code_task' && pageId) {
const taskReceiptPath = `.memind/agent-runs/${safeRequestId}-page-edit.json`;
expectedFiles.push({ path: taskReceiptPath, contains: pageId });
instructions.push(
'',
'[Memind page-edit validation]',
`Before finishing this page-edit task, create or update ${taskReceiptPath}.`,
'The file must be valid JSON and include:',
`- requestId: ${requestId}`,
`- taskType: ${taskType}`,
`- pageId: ${pageId}`,
...(pageTitle ? [`- pageTitle: ${pageTitle}`] : []),
'- a brief summary of the page changes or the reason no change was needed.',
);
}
if (taskType === PAGE_DATA_DEV_TASK_TYPE) {
const taskReceiptPath = `.memind/agent-runs/${safeRequestId}-page-data-dev.json`;
expectedFiles.push({ path: taskReceiptPath, contains: PAGE_DATA_DEV_TASK_TYPE });
instructions.push(
'',
'[Memind page-data-dev validation]',
`Before finishing this Page Data dev repair task, create or update ${taskReceiptPath}.`,
'The file must be valid JSON and include:',
`- requestId: ${requestId}`,
`- taskType: ${PAGE_DATA_DEV_TASK_TYPE}`,
'- a brief summary of HTML/policy fixes or why no file change was needed.',
'Only modify public/*.html and .mindspace/page-data-policies/*.json unless explicitly told otherwise.',
'Do not recreate datasets or call private_data_execute unless the user pasted schema errors.',
);
}
return {
validation: expectedFiles.length ? { expectedFiles } : null,
instruction: instructions.join('\n'),
};
}
export function buildAgentRunValidationReceipt(requestId: string): {
validation: AgentRunValidation;
instruction: string;
} {
const normalizedRequestId = String(requestId ?? '').trim();
const safeRequestId = sanitizeRequestIdForPath(normalizedRequestId);
const receiptPath = `.memind/agent-runs/${safeRequestId}.json`;
const marker = normalizedRequestId || safeRequestId;
return {
validation: {
expectedFile: {
path: receiptPath,
contains: marker,
},
},
instruction: [
'',
'[Memind code-run validation]',
`Before finishing, create or update ${receiptPath}.`,
`The file must be valid JSON and include this requestId value: ${marker}.`,
'Do not skip this receipt even if the user task itself is complete.',
].join('\n'),
};
}
export function resolveAgentRunOptions(
text: string,
{
taskType = 'code_task',
forceCode = false,
allowAutodetect = clientCodeRunsAutodetectEnabled(),
allowPageDataDevAutodetect = clientPageDataDevAutodetectEnabled(),
userId = null,
requestId = null,
mindspaceContext = null,
pageEdit = null,
}: {
taskType?: string;
forceCode?: boolean;
allowAutodetect?: boolean;
allowPageDataDevAutodetect?: boolean;
userId?: string | null;
requestId?: string | null;
mindspaceContext?: MindSpaceChatContext | null;
pageEdit?: { pageId?: string | null; pageTitle?: string | null } | null;
} = {},
): AgentRunCreateOptions {
const normalizedText = String(text ?? '').trim();
const pageDataDevTaskType =
allowPageDataDevAutodetect && resolvePageDataDevTaskType(normalizedText);
const effectiveTaskType = pageDataDevTaskType ?? taskType;
const shouldUseDeepReasoning =
forceCode ||
Boolean(pageDataDevTaskType) ||
DEEP_REASONING_TASK_PATTERNS.some((pattern) => pattern.test(normalizedText));
if (!agentCodeRunsEnabledForUser(userId)) {
return shouldUseDeepReasoning ? { forceDeepReasoning: true, taskType: effectiveTaskType } : {};
}
const shouldUseCode =
forceCode ||
Boolean(pageDataDevTaskType) ||
(allowAutodetect && CODE_TASK_PATTERNS.some((pattern) => pattern.test(normalizedText)));
if (!shouldUseCode) {
return shouldUseDeepReasoning ? { forceDeepReasoning: true, taskType: effectiveTaskType } : {};
}
const normalizedRequestId = requestId ?? crypto.randomUUID();
const receipt = buildAgentRunValidationReceipt(normalizedRequestId);
const taskValidation = buildAgentRunTaskValidation({
requestId: normalizedRequestId,
taskType: effectiveTaskType,
text: normalizedText,
mindspaceContext,
pageEdit,
});
return {
toolMode: 'code',
taskType: effectiveTaskType,
forceDeepReasoning: true,
validation: mergeAgentRunValidationFiles(receipt.validation, taskValidation.validation),
validationInstruction: `${receipt.instruction}${taskValidation.instruction}`,
};
}