feat: add system disclosure policy gate
Memind CI / Test, build, and release guards (pull_request) Successful in 3m14s
Memind CI / Test, build, and release guards (pull_request) Successful in 3m14s
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user