Files
memind/system-disclosure-policy.mjs
john 205b5fd8be
Memind CI / Test, build, and release guards (pull_request) Successful in 3m14s
feat: add system disclosure policy gate
2026-07-23 22:53:15 +08:00

413 lines
14 KiB
JavaScript

import { deriveUserFacingText } from './conversation-display.mjs';
const CONFIG_TABLE = 'h5_system_disclosure_policy_config';
const CONFIG_SCOPE = 'global';
const POLICY_ID = 'system-disclosure';
const DEFAULT_REFRESH_INTERVAL_MS = 5000;
export const SYSTEM_DISCLOSURE_MODE = Object.freeze({
OFF: 'off',
SHADOW: 'shadow',
ENFORCE: 'enforce',
});
export const DEFAULT_SYSTEM_DISCLOSURE_REFUSAL =
'出于安全与隐私考虑,我不能提供 TKMind、Memind 或 MindSpace 的内部技术实现信息。我可以继续帮助你了解公开功能、使用方法、服务边界,或者讨论不针对本系统的一般性技术原理。';
const DEFAULT_PRODUCT_NAMES = Object.freeze([
'tkmind',
'memind',
'mindspace',
'智趣',
]);
const DEFAULT_SELF_REFERENCES = Object.freeze([
'本系统',
'这个系统',
'该系统',
'你们系统',
'你们的系统',
'你们平台',
'你们的平台',
'这个平台',
'该平台',
'你的系统',
'你的平台',
'你们产品',
'你们的产品',
]);
const TECHNICAL_CATEGORY_PATTERNS = Object.freeze({
architecture: [
/(?:底层|内部|系统|技术|整体|服务|运行时|agent)\s*(?:架构|设计|实现|原理|机制)/iu,
/(?:架构|设计|实现|原理|机制)\s*(?:图|说明|细节|文档|方案|是什|怎么|如何)/iu,
/(?:技术栈|开发语言|编程语言|前端框架|后端框架|开源组件|内部组件|依赖版本|运行框架)/iu,
/(?:tkmind|memind|mindspace).{0,24}(?:关系|区别|协作|调用链|怎么工作|如何工作)/iu,
/\b(?:architecture|internals?|implementation|system design|runtime design)\b/iu,
],
model_and_routing: [
/(?:模型|大模型|llm|provider|供应商).{0,18}(?:选择|路由|调用|切换|配置|名称|版本|怎么|如何|哪些|什么)/iu,
/(?:使用|采用|接入|调用).{0,12}(?:什么|哪个|哪些)?(?:模型|大模型|llm|agent\s*框架)/iu,
/(?:意图识别|意图路由|任务路由|模型路由|agent\s*编排|智能体编排)/iu,
/\b(?:model routing|intent routing|agent orchestration|provider routing)\b/iu,
],
prompts_and_memory: [
/(?:系统提示词|system\s*prompt|隐藏提示词|内部提示词|开发者提示词|developer\s*message)/iu,
/(?:记忆|memory|上下文).{0,18}(?:注入|召回|存储|实现|机制|路由|拼接|读取)/iu,
],
tools_and_extensions: [
/(?:工具|扩展|插件|skill|mcp).{0,18}(?:清单|列表|数量|名称|调用|编排|配置|内部|有哪些|多少)/iu,
/(?:agent|智能体).{0,18}(?:数量|几个|名称|分工|清单|列表|怎么协作|如何协作)/iu,
/(?:几个|多少|哪些).{0,8}(?:agent|智能体|工具|扩展|插件)/iu,
/(?:sandbox-fs|developer\s*tool|tool\s*gateway|extension\s*override)/iu,
],
data_and_deployment: [
/(?:数据库|存储|数据层|表结构|schema).{0,18}(?:类型|选型|结构|实现|位置|连接|配置|怎么|如何|什么)/iu,
/(?:数据|文件|记忆).{0,12}(?:存在哪里|保存在哪|落在哪里|怎么保存|如何保存)/iu,
/(?:部署|拓扑|服务器|主机|端口|容器|运行目录|源码目录|仓库路径|内部\s*api|内部接口)/iu,
/(?:源码|代码|仓库).{0,12}(?:开源|位置|目录|结构|地址|在哪)/iu,
/\b(?:deployment|topology|database schema|source tree|internal api|host|port)\b/iu,
],
security_boundaries: [
/(?:沙箱|权限|安全边界|防护|鉴权|认证|密钥|token).{0,18}(?:实现|机制|策略|配置|绕过|细节|怎么|如何)/iu,
/(?:越权|绕过|突破|规避).{0,18}(?:限制|防护|沙箱|鉴权|策略)/iu,
/\b(?:security boundary|sandbox escape|bypass|auth internals)\b/iu,
],
});
const IMPLICIT_SELF_TECHNICAL_PATTERNS = Object.freeze([
/你(?:们)?(?:到底)?(?:使用|采用|接入|调用|运行).{0,12}(?:什么|哪个|哪些)?(?:模型|大模型|llm|数据库|框架|技术栈)/iu,
/你(?:们)?(?:使用|用了|有|部署).{0,8}(?:几个|多少|哪些).{0,8}(?:agent|智能体|工具|扩展|插件)/iu,
/你(?:们)?(?:的)?(?:底层|内部|后台).{0,12}(?:架构|实现|原理|机制|模型|技术栈)/iu,
/你(?:是|们是)?(?:怎么|如何).{0,18}(?:路由|编排|调用工具|保存记忆|部署|存储数据)/iu,
]);
function clone(value) {
if (typeof structuredClone === 'function') return structuredClone(value);
return JSON.parse(JSON.stringify(value));
}
function normalizeBoolean(value, fallback = false) {
if (value == null || value === '') return fallback;
if (typeof value === 'boolean') return value;
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
}
function normalizeMode(value, fallback = SYSTEM_DISCLOSURE_MODE.SHADOW) {
const normalized = String(value ?? '').trim().toLowerCase();
return Object.values(SYSTEM_DISCLOSURE_MODE).includes(normalized)
? normalized
: fallback;
}
function normalizeStringList(value, fallback) {
if (!Array.isArray(value)) return [...fallback];
const items = [...new Set(value.map((item) => String(item ?? '').trim()).filter(Boolean))];
return items.length ? items.slice(0, 100) : [...fallback];
}
function normalizeRefusalText(value) {
const normalized = String(value ?? '').trim();
return normalized
? normalized.slice(0, 1000)
: DEFAULT_SYSTEM_DISCLOSURE_REFUSAL;
}
export function defaultSystemDisclosureConfig() {
return {
enabled: true,
mode: SYSTEM_DISCLOSURE_MODE.SHADOW,
refusalText: DEFAULT_SYSTEM_DISCLOSURE_REFUSAL,
productNames: [...DEFAULT_PRODUCT_NAMES],
selfReferences: [...DEFAULT_SELF_REFERENCES],
categories: Object.fromEntries(
Object.keys(TECHNICAL_CATEGORY_PATTERNS).map((key) => [key, true]),
),
};
}
export function normalizeSystemDisclosureConfig(input = {}, fallback = defaultSystemDisclosureConfig()) {
const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
const base = fallback && typeof fallback === 'object' ? fallback : defaultSystemDisclosureConfig();
const next = {
enabled: normalizeBoolean(source.enabled, base.enabled),
mode: normalizeMode(source.mode, base.mode),
refusalText: normalizeRefusalText(source.refusalText ?? base.refusalText),
productNames: normalizeStringList(source.productNames, base.productNames),
selfReferences: normalizeStringList(source.selfReferences, base.selfReferences),
categories: {},
};
for (const key of Object.keys(TECHNICAL_CATEGORY_PATTERNS)) {
next.categories[key] = normalizeBoolean(source.categories?.[key], base.categories?.[key] !== false);
}
if (!next.enabled) next.mode = SYSTEM_DISCLOSURE_MODE.OFF;
return next;
}
function parseJsonLike(value, fallback) {
if (value == null || value === '') return fallback;
if (typeof value === 'object') return value;
try {
return JSON.parse(String(value));
} catch {
return fallback;
}
}
function normalizeText(value) {
return String(value ?? '')
.normalize('NFKC')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
function includesAny(text, values) {
return values.some((value) => text.includes(normalizeText(value)));
}
function matchingCategories(text, config) {
const matched = [];
for (const [category, patterns] of Object.entries(TECHNICAL_CATEGORY_PATTERNS)) {
if (config.categories?.[category] === false) continue;
if (patterns.some((pattern) => pattern.test(text))) matched.push(category);
}
return matched;
}
export function extractSystemDisclosureMessageText(message) {
const displayText = String(message?.metadata?.displayText ?? '').trim();
if (displayText) return deriveUserFacingText(displayText);
if (typeof message?.content === 'string') return deriveUserFacingText(message.content);
if (!Array.isArray(message?.content)) {
return deriveUserFacingText(message?.text ?? message?.value ?? '');
}
return deriveUserFacingText(message.content
.filter((item) => item?.type === 'text')
.map((item) => String(item.text ?? '').trim())
.filter(Boolean)
.join('\n'));
}
export function evaluateSystemDisclosureText(text, configInput = defaultSystemDisclosureConfig()) {
const config = normalizeSystemDisclosureConfig(configInput);
const normalizedText = normalizeText(text);
const base = {
policyId: POLICY_ID,
policyVersion: null,
mode: config.mode,
action: 'allow',
matched: false,
enforced: false,
reasonCode: null,
categories: [],
responseText: null,
};
if (!config.enabled || config.mode === SYSTEM_DISCLOSURE_MODE.OFF || !normalizedText) {
return base;
}
const categories = matchingCategories(normalizedText, config);
if (!categories.length) return base;
const explicitProduct = includesAny(normalizedText, config.productNames);
const explicitSelfReference = includesAny(normalizedText, config.selfReferences);
const implicitSelfTechnical = IMPLICIT_SELF_TECHNICAL_PATTERNS.some((pattern) =>
pattern.test(normalizedText));
if (!explicitProduct && !explicitSelfReference && !implicitSelfTechnical) return base;
const enforced = config.mode === SYSTEM_DISCLOSURE_MODE.ENFORCE;
return {
...base,
action: enforced ? 'refuse' : 'allow',
wouldAction: 'refuse',
matched: true,
enforced,
reasonCode: 'SYSTEM_TECHNICAL_DISCLOSURE',
categories,
responseText: enforced ? config.refusalText : null,
};
}
async function ensureConfigTable(pool) {
await pool.query(`
CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} (
config_scope VARCHAR(32) PRIMARY KEY,
config_json JSON NOT NULL,
policy_version BIGINT NOT NULL DEFAULT 1,
updated_by CHAR(36) NULL,
updated_at BIGINT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
}
async function loadStoredState(pool) {
await ensureConfigTable(pool);
const [rows] = await pool.query(
`SELECT config_json, policy_version, updated_by, updated_at
FROM ${CONFIG_TABLE}
WHERE config_scope = ?
LIMIT 1`,
[CONFIG_SCOPE],
);
const row = rows[0];
if (!row) return null;
return {
config: normalizeSystemDisclosureConfig(
parseJsonLike(row.config_json, defaultSystemDisclosureConfig()),
),
policyVersion: Math.max(1, Number(row.policy_version ?? 1) || 1),
updatedBy: row.updated_by ?? null,
updatedAt: Number(row.updated_at ?? 0) || null,
};
}
export function createSystemDisclosurePolicyService(
pool,
{
refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS,
logger = console,
autoRefresh = true,
} = {},
) {
let state = {
config: defaultSystemDisclosureConfig(),
policyVersion: 0,
updatedBy: null,
updatedAt: null,
source: 'default',
refreshedAt: null,
lastRefreshError: null,
};
let refreshTimer = null;
let refreshPromise = null;
async function refresh() {
if (refreshPromise) return refreshPromise;
refreshPromise = (async () => {
try {
const stored = await loadStoredState(pool);
state = {
config: stored?.config ?? defaultSystemDisclosureConfig(),
policyVersion: stored?.policyVersion ?? 0,
updatedBy: stored?.updatedBy ?? null,
updatedAt: stored?.updatedAt ?? null,
source: stored ? 'admin-db' : 'default',
refreshedAt: Date.now(),
lastRefreshError: null,
};
} catch (error) {
state = {
...state,
refreshedAt: Date.now(),
lastRefreshError: error instanceof Error ? error.message : String(error),
};
logger?.warn?.(
'[system-disclosure-policy] refresh failed; keeping last-known-good snapshot:',
state.lastRefreshError,
);
} finally {
refreshPromise = null;
}
return clone(state);
})();
return refreshPromise;
}
function startAutoRefresh() {
if (!autoRefresh || refreshTimer || refreshIntervalMs <= 0) return;
refreshTimer = setInterval(() => {
void refresh();
}, refreshIntervalMs);
refreshTimer.unref?.();
}
function stopAutoRefresh() {
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = null;
}
return {
async initialize() {
await refresh();
startAutoRefresh();
return this.getRuntimeState();
},
close() {
stopAutoRefresh();
},
evaluate({ text, userMessage } = {}) {
const decision = evaluateSystemDisclosureText(
text ?? extractSystemDisclosureMessageText(userMessage),
state.config,
);
return {
...decision,
policyVersion: state.policyVersion,
};
},
async refresh() {
return refresh();
},
getRuntimeState() {
return clone(state);
},
async getAdminConfig() {
const stored = await loadStoredState(pool);
return {
config: stored?.config ?? defaultSystemDisclosureConfig(),
policyVersion: stored?.policyVersion ?? 0,
updatedBy: stored?.updatedBy ?? null,
updatedAt: stored?.updatedAt ?? null,
source: stored ? 'admin-db' : 'default',
};
},
async updateAdminConfig(patch = {}, { updatedBy = null } = {}) {
const stored = await loadStoredState(pool);
const current = stored?.config ?? defaultSystemDisclosureConfig();
const patchConfig = patch?.config ?? patch;
const merged = normalizeSystemDisclosureConfig({
...current,
...patchConfig,
categories: {
...current.categories,
...(patchConfig?.categories ?? {}),
},
}, current);
const nextVersion = (stored?.policyVersion ?? 0) + 1;
const now = Date.now();
await ensureConfigTable(pool);
await pool.query(
`INSERT INTO ${CONFIG_TABLE}
(config_scope, config_json, policy_version, updated_by, updated_at)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
config_json = VALUES(config_json),
policy_version = VALUES(policy_version),
updated_by = VALUES(updated_by),
updated_at = VALUES(updated_at)`,
[CONFIG_SCOPE, JSON.stringify(merged), nextVersion, updatedBy, now],
);
await refresh();
return this.getAdminConfig();
},
};
}
export const systemDisclosurePolicyInternals = {
CONFIG_SCOPE,
CONFIG_TABLE,
POLICY_ID,
TECHNICAL_CATEGORY_PATTERNS,
IMPLICIT_SELF_TECHNICAL_PATTERNS,
};