feat: add system disclosure policy gate
Memind CI / Test, build, and release guards (pull_request) Successful in 3m14s

This commit is contained in:
john
2026-07-23 22:53:15 +08:00
parent ba6cc10f08
commit 205b5fd8be
20 changed files with 1585 additions and 2 deletions
+6
View File
@@ -20,6 +20,7 @@ import { createAssetGatewayConfigService } from './asset-gateway.mjs';
import { createImageMakeAdminConfigService } from './image-make-admin-config.mjs';
import { createMemoryV2AdminConfigService } from './memory-v2-admin-config.mjs';
import { createSkillRuntimeAdminConfigService } from './skill-runtime-admin-config.mjs';
import { createSystemDisclosurePolicyService } from './system-disclosure-policy.mjs';
import { createMindSearchConfigService } from './mindsearch-config.mjs';
import { createWechatScheduleLlmConfigService } from './wechat-schedule-llm-config.mjs';
import { createPlazaPostService, formatPostRow } from './plaza-posts.mjs';
@@ -111,6 +112,10 @@ export async function createAdminServices(env = {}) {
const mindSearchConfigService = createMindSearchConfigService(pool);
await mindSearchConfigService.ensureSchema();
const skillRuntimeConfigService = createSkillRuntimeAdminConfigService(pool, { h5Root });
const systemDisclosurePolicyService = createSystemDisclosurePolicyService(pool, {
autoRefresh: false,
});
await systemDisclosurePolicyService.initialize();
const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool);
const adminSystemTestService = createAdminSystemTestService({
pool,
@@ -152,6 +157,7 @@ export async function createAdminServices(env = {}) {
memoryV2ConfigService,
mindSearchConfigService,
skillRuntimeConfigService,
systemDisclosurePolicyService,
wechatScheduleLlmConfigService,
adminSystemTestService,
plazaPosts,
+29
View File
@@ -32,6 +32,7 @@ function plazaRouteError(res, req, error) {
* @param {object|null} deps.llmProviderService
* @param {object|null} deps.memoryV2ConfigService
* @param {object|null} deps.skillRuntimeConfigService
* @param {object|null} deps.systemDisclosurePolicyService
* @param {object|null} deps.adminSystemTestService
* @param {object|null} deps.plazaPosts
* @param {object|null} deps.plazaOps
@@ -48,6 +49,7 @@ export function createAdminApi({
memoryV2ConfigService,
mindSearchConfigService,
skillRuntimeConfigService,
systemDisclosurePolicyService,
adminSystemTestService,
plazaPosts,
plazaOps,
@@ -234,6 +236,33 @@ export function createAdminApi({
adminApi.put('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig);
adminApi.patch('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig);
adminApi.get('/system-disclosure-policy/config', requireAdmin, async (_req, res) => {
if (!systemDisclosurePolicyService?.getAdminConfig) {
return res.status(503).json({ message: 'System Disclosure Policy 服务未启用' });
}
return res.json(await systemDisclosurePolicyService.getAdminConfig());
});
const updateSystemDisclosurePolicy = async (req, res) => {
if (!systemDisclosurePolicyService?.updateAdminConfig) {
return res.status(503).json({ message: 'System Disclosure Policy 服务未启用' });
}
const result = await systemDisclosurePolicyService.updateAdminConfig(req.body?.config ?? req.body ?? {}, {
updatedBy: req.currentUser.id,
});
return res.json(result);
};
adminApi.put('/system-disclosure-policy/config', requireAdmin, updateSystemDisclosurePolicy);
adminApi.patch('/system-disclosure-policy/config', requireAdmin, updateSystemDisclosurePolicy);
adminApi.get('/system-disclosure-policy/runtime', requireAdmin, async (_req, res) => {
if (!systemDisclosurePolicyService?.getRuntimeState) {
return res.status(503).json({ message: 'System Disclosure Policy 服务未启用' });
}
return res.json(systemDisclosurePolicyService.getRuntimeState());
});
adminApi.get('/skill-runtime/catalog', requireAdmin, async (_req, res) => {
if (!skillRuntimeConfigService?.listCatalogSummary) {
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
+73
View File
@@ -90,6 +90,79 @@ test('admin memory-v2 config routes expose config and runtime state', async () =
}
});
test('admin system disclosure policy routes version config through the injected control-plane service', async () => {
const updates = [];
const config = {
enabled: true,
mode: 'shadow',
refusalText: '不提供内部技术信息。',
productNames: ['tkmind'],
selfReferences: ['本系统'],
categories: { architecture: true },
};
const router = createAdminApi({
jsonBody: express.json(),
getToken() {
return 'token-admin';
},
userAuth: {
async getMe() {
return { id: 'admin-1', role: 'admin' };
},
},
llmProviderService: null,
memoryV2ConfigService: null,
systemDisclosurePolicyService: {
async getAdminConfig() {
return { config, policyVersion: 2, source: 'admin-db' };
},
async updateAdminConfig(patch, context) {
updates.push({ patch, context });
return { config: patch, policyVersion: 3, source: 'admin-db' };
},
getRuntimeState() {
return { config, policyVersion: 2, source: 'admin-db', refreshedAt: 123 };
},
},
plazaPosts: null,
plazaOps: null,
wechatAdmin: null,
subscriptionService: null,
});
const server = await startTestServer(router);
try {
const getResponse = await fetch(`${server.baseUrl}/admin-api/system-disclosure-policy/config`, {
headers: { cookie: 'h5_user_session=token-admin' },
});
assert.equal(getResponse.status, 200);
assert.equal((await getResponse.json()).policyVersion, 2);
const updateResponse = await fetch(`${server.baseUrl}/admin-api/system-disclosure-policy/config`, {
method: 'PUT',
headers: {
'content-type': 'application/json',
cookie: 'h5_user_session=token-admin',
},
body: JSON.stringify({ config: { ...config, mode: 'enforce' } }),
});
assert.equal(updateResponse.status, 200);
assert.equal((await updateResponse.json()).policyVersion, 3);
assert.deepEqual(updates, [{
patch: { ...config, mode: 'enforce' },
context: { updatedBy: 'admin-1' },
}]);
const runtimeResponse = await fetch(`${server.baseUrl}/admin-api/system-disclosure-policy/runtime`, {
headers: { cookie: 'h5_user_session=token-admin' },
});
assert.equal(runtimeResponse.status, 200);
assert.equal((await runtimeResponse.json()).refreshedAt, 123);
} finally {
await server.close();
}
});
test('admin MindSearch routes persist only through injected control-plane service', async () => {
const updates = [];
const router = createAdminApi({
+1
View File
@@ -80,6 +80,7 @@ const CONSOLES = {
imageMakeAdminConfigService: services.imageMakeAdminConfigService,
memoryV2ConfigService: services.memoryV2ConfigService,
mindSearchConfigService: services.mindSearchConfigService,
systemDisclosurePolicyService: services.systemDisclosurePolicyService,
adminSystemTestService: services.adminSystemTestService,
plazaPosts: services.plazaPosts,
plazaOps: services.plazaOps,
+64 -1
View File
@@ -283,6 +283,7 @@ export function createAgentRunGateway({
tkmindProxy,
toolGateway = null,
directChatService = null,
systemDisclosurePolicyService = null,
chatIntentRouter = null,
sessionSnapshotService = null,
conversationMemoryService = null,
@@ -596,6 +597,60 @@ export function createAgentRunGateway({
let userMessage = safeJsonParse(row.user_message_json, {});
const runOptions = getRunOptionsFromMessage(userMessage);
const toolGatewayStatus = toolGateway?.getStatus ? toolGateway.getStatus() : null;
let disclosureDecision = null;
try {
disclosureDecision = systemDisclosurePolicyService?.evaluate?.({
userMessage,
channel: 'h5',
userId: row.user_id,
sessionId: row.agent_session_id ?? null,
}) ?? null;
} catch (err) {
console.warn(
'[AgentRun] system disclosure policy evaluation failed open:',
err instanceof Error ? err.message : err,
);
}
if (disclosureDecision?.enforced) {
if (!directChatService?.respondDeterministically) {
const error = new Error('System Disclosure Policy refusal service unavailable');
error.code = 'SYSTEM_DISCLOSURE_REFUSAL_UNAVAILABLE';
error.retryable = false;
throw error;
}
const result = await directChatService.respondDeterministically({
userId: row.user_id,
sessionId: row.agent_session_id ?? null,
requestId: row.request_id,
userMessage,
reply: disclosureDecision.responseText,
metadata: {
policyId: disclosureDecision.policyId,
policyVersion: disclosureDecision.policyVersion,
policyReasonCode: disclosureDecision.reasonCode,
},
onSessionReady: async (activeSessionId) => {
await pool.query(
`UPDATE h5_agent_runs SET agent_session_id = ?, updated_at = ? WHERE id = ?`,
[activeSessionId, nowMs(), runId],
);
await appendRunSnapshot(runId);
},
});
await appendEvent(runId, 'system_disclosure_blocked', {
policyId: disclosureDecision.policyId,
policyVersion: disclosureDecision.policyVersion,
reasonCode: disclosureDecision.reasonCode,
categories: disclosureDecision.categories,
channel: 'h5',
});
return {
sessionId: result.sessionId,
routing: null,
toolEvidence: null,
policyBlocked: true,
};
}
const routing = await resolveRunRouting(row, userMessage, runOptions);
const routingDecision = resolveLegacyRouteFromClassification(routing) ?? routing?.route ?? null;
let agentMemoryContext = null;
@@ -867,8 +922,16 @@ export function createAgentRunGateway({
runId,
row,
sessionId,
{ routing = null, toolEvidence = null } = {},
{ routing = null, toolEvidence = null, policyBlocked = false } = {},
) {
if (policyBlocked) {
await markRun(runId, 'succeeded', {
agent_session_id: sessionId,
completed_at: nowMs(),
error_message: null,
}, { expectedStatus: 'running' });
return;
}
assertRequiredImageGenerationCompleted(row, routing, toolEvidence);
// `row` was loaded before this worker claimed the run, so its started_at
// can still be null. Refresh it before scoping workspace files to the
+124
View File
@@ -401,6 +401,130 @@ test('agent run starts a session and marks submitted reply as succeeded', async
assert.equal(pool.runs.get(run.id).attempts, 1);
});
test('agent run policy allow path preserves existing routing and submission behavior', async () => {
const pool = createFakePool();
const submitted = [];
const evaluated = [];
const routed = [];
const gateway = createAgentRunGateway({
pool,
userAuth: {},
systemDisclosurePolicyService: {
evaluate(input) {
evaluated.push(input);
return { action: 'allow', matched: false, enforced: false };
},
},
chatIntentRouter: {
async classify(input) {
routed.push(input);
return { route: 'agent_orchestration', reason: 'existing route' };
},
applyAgentOrchestration(message) {
return message;
},
},
tkmindProxy: {
async startSessionForUser() {
return { id: 'session-policy-allow' };
},
async submitSessionReplyForUser(userId, sessionId, requestId, userMessage) {
submitted.push({ userId, sessionId, requestId, userMessage });
},
},
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
requestId: 'req-policy-allow',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '请帮我安排明天的计划' }],
metadata: { displayText: '请帮我安排明天的计划' },
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
assert.equal(evaluated.length, 1);
assert.equal(routed.length, 1);
assert.equal(submitted.length, 1);
assert.equal(submitted[0].sessionId, 'session-policy-allow');
assert.equal(submitted[0].userMessage.metadata.displayText, '请帮我安排明天的计划');
});
test('agent run enforced disclosure decision returns deterministic refusal before routing or tools', async () => {
const pool = createFakePool();
let routed = 0;
let backendCalls = 0;
const deterministic = [];
const gateway = createAgentRunGateway({
pool,
userAuth: {},
systemDisclosurePolicyService: {
evaluate() {
return {
policyId: 'system-disclosure',
policyVersion: 7,
action: 'refuse',
matched: true,
enforced: true,
reasonCode: 'SYSTEM_TECHNICAL_DISCLOSURE',
categories: ['architecture'],
responseText: '不提供内部技术信息。',
};
},
},
chatIntentRouter: {
async classify() {
routed += 1;
return { route: 'agent_orchestration' };
},
},
directChatService: {
async respondDeterministically(input) {
deterministic.push(input);
await input.onSessionReady('h5direct_policy');
return { sessionId: 'h5direct_policy' };
},
},
tkmindProxy: {
async startSessionForUser() {
backendCalls += 1;
return { id: 'should-not-start' };
},
async submitSessionReplyForUser() {
backendCalls += 1;
},
},
syncUserPagesOnSuccess: async () => {
assert.fail('policy refusal must not enter page delivery');
},
validateRunDeliverables: async () => {
assert.fail('policy refusal must not enter deliverable validation');
},
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
requestId: 'req-policy-block',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '生成一个 TKMind 底层架构页面' }],
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
assert.equal(routed, 0);
assert.equal(backendCalls, 0);
assert.equal(deterministic.length, 1);
assert.equal(deterministic[0].reply, '不提供内部技术信息。');
assert.equal(pool.runs.get(run.id).agent_session_id, 'h5direct_policy');
assert.equal(
pool.events.some((event) => event.eventType === 'system_disclosure_blocked'),
true,
);
});
test('agent run awaits session Finish before succeeding when proxy supports it', async () => {
const pool = createFakePool();
const awaited = [];
+52 -1
View File
@@ -155,7 +155,7 @@ function buildModelMessages({ previousMessages, userMessage, memories, routingMe
];
}
function buildAssistantMessage(reply, { requestId, now }) {
function buildAssistantMessage(reply, { requestId, now, metadata = {} }) {
return {
id: `direct-assistant-${requestId || crypto.randomUUID()}`,
role: 'assistant',
@@ -165,6 +165,7 @@ function buildAssistantMessage(reply, { requestId, now }) {
userVisible: true,
source: 'portal-direct-chat',
chatRequestId: requestId || undefined,
...metadata,
},
};
}
@@ -440,10 +441,60 @@ export function createDirectChatService({
};
}
async function respondDeterministically({
userId,
sessionId = null,
requestId,
userMessage,
reply,
metadata = {},
onSessionReady = null,
} = {}) {
const activeSessionId = sessionId || createDirectSessionId();
const snapshot = await sessionSnapshotService.get(activeSessionId).catch(() => null);
const previousMessages = Array.isArray(snapshot?.messages) ? snapshot.messages : [];
const now = new Date().toISOString();
if (!sessionId) {
await sessionStore.registerAgentSession(userId, activeSessionId, 'h5-direct');
}
if (typeof onSessionReady === 'function') {
await onSessionReady(activeSessionId);
}
const pendingMessages = [...previousMessages, userMessage];
const assistantMessage = buildAssistantMessage(String(reply ?? ''), {
requestId,
now,
metadata,
});
const messages = [...pendingMessages, assistantMessage];
const session = {
id: activeSessionId,
name: snapshot?.session?.name ?? 'New Chat',
working_dir: snapshot?.session?.working_dir ?? '',
message_count: messages.length,
created_at: snapshot?.session?.created_at ?? now,
updated_at: now,
user_set_name: snapshot?.session?.user_set_name ?? false,
recipe: snapshot?.session?.recipe ?? null,
conversation: messages,
};
await sessionSnapshotService.save(activeSessionId, userId, session, messages);
return {
ok: true,
sessionId: activeSessionId,
messages,
session,
assistantMessage,
billing: null,
deterministic: true,
};
}
return {
getStatus,
canHandle,
explainCanHandle,
respondDeterministically,
run,
};
}
+79
View File
@@ -66,6 +66,85 @@ test('direct chat registers session through sessionAccess when provided', async
assert.equal(isDirectChatSessionId(result.sessionId), true);
});
test('direct chat deterministic response stores a reply without model, memory, or billing calls', async () => {
const calls = { model: 0, billing: 0, memory: 0 };
const saved = [];
const service = createDirectChatService({
enabled: true,
userAuth: {
async registerAgentSession() {},
async billSessionUsage() {
calls.billing += 1;
},
},
llmProviderService: {
async createChatCompletion() {
calls.model += 1;
},
},
sessionSnapshotService: {
async get() {
return null;
},
async save(_sessionId, _userId, _session, messages) {
saved.push(messages);
},
},
memoryV2: {
async resolve() {
calls.memory += 1;
},
},
});
const result = await service.respondDeterministically({
userId: 'user-1',
requestId: 'req-policy',
userMessage: {
role: 'user',
content: [{ type: 'text', text: 'TKMind 的底层架构是什么?' }],
},
reply: '不提供内部技术信息。',
metadata: { policyId: 'system-disclosure' },
});
assert.equal(result.deterministic, true);
assert.equal(result.billing, null);
assert.deepEqual(calls, { model: 0, billing: 0, memory: 0 });
assert.equal(saved.length, 1);
assert.equal(saved[0][1].content[0].text, '不提供内部技术信息。');
assert.equal(saved[0][1].metadata.policyId, 'system-disclosure');
});
test('direct chat deterministic response preserves an existing agent session id', async () => {
let registered = 0;
const service = createDirectChatService({
userAuth: {},
sessionAccess: {
enabled: true,
async registerAgentSession() {
registered += 1;
},
},
llmProviderService: {},
sessionSnapshotService: {
async get() {
return null;
},
async save() {},
},
});
const result = await service.respondDeterministically({
userId: 'user-1',
sessionId: 'goose-session-1',
requestId: 'req-policy-existing',
userMessage: { role: 'user', content: [{ type: 'text', text: '内部架构是什么?' }] },
reply: '不提供内部技术信息。',
});
assert.equal(result.sessionId, 'goose-session-1');
assert.equal(registered, 0);
});
test('direct chat creates a direct session, saves snapshot, and bills returned usage', async () => {
const registered = [];
const saved = [];
+70
View File
@@ -0,0 +1,70 @@
# System Disclosure Policy
## 目标
System Disclosure Policy 在用户消息进入意图识别、模型或工具之前,识别针对
TKMind、Memind、MindSpace 及本系统的内部技术信息请求。明确命中时返回固定拒答,
不调用模型、不执行工具、不计费。
该策略由 memindadm 的「策略中心」管理,Portal 运行时只读取内存快照。后台配置与
运行时执行分离,避免每条用户消息查询数据库。
## Zero-Impact Allow Invariant
未命中请求必须满足:
1. 不修改用户消息或 metadata。
2. 不注入 System Prompt、Developer Prompt 或路由前缀。
3. 不改变意图识别、模型选择、Session、Agent Run、工具与发布流程。
4. 不增加模型调用或计费。
5. 策略判断异常时保留现有流程(fail open),配置刷新异常时继续使用最后一个有效快照。
任何修改策略接入点的提交,都必须用测试证明 allow 路径仍进入原路由和提交逻辑。
## 执行位置
- H5`agent-run-gateway.mjs``executeRun`,位于 `resolveRunRouting` 之前。
- 微信服务号:`wechat-mp.mjs`,位于日程分流和 Agent Session 创建之前。
- Goose 后备闸门:`tkmind-proxy.mjs``prepareSessionReplyBody`,位于鉴权、
计费检查、Session reconcile 和模型调用之前。
H5 拒答通过 direct-chat snapshot 写入确定性助手消息,保持现有聊天事件与历史记录
交付方式;拒答路径跳过页面交付检查和个人记忆观察。
## 运行模式
- `off`:关闭判断。
- `shadow`:只计算命中,不改变结果。默认模式。
- `enforce`:明确命中后固定拒答。
配置以版本号写入 `h5_system_disclosure_policy_config`。Portal 启动时加载,随后定时
原子替换内存快照;刷新失败时继续使用 last-known-good。
## 当前规则边界
只有「本系统指向」和「内部技术类别」同时命中才拒答:
- 本系统指向:产品名(TKMind、Memind、MindSpace、智趣)或“本系统”“你们平台”等自指。
- 技术类别:架构实现、模型路由、提示词记忆、工具扩展、数据部署、安全边界。
一般性技术讨论以及公开功能使用问题继续放行,例如:
- “如何设计一个多 Agent 系统架构?”
- “MindSpace 页面怎么发布和分享?”
第一阶段不把法律合规规则混入本模块。后续可以在 Trust & Policy Center 增加独立
policy domain,并复用相同的只读判断、版本、shadow、enforce 和回退契约。
## 验证
```bash
node --test \
system-disclosure-policy.test.mjs \
direct-chat-service.test.mjs \
agent-run-gateway.test.mjs \
admin-routes.test.mjs \
wechat-mp.test.mjs \
tkmind-proxy.test.mjs
cd ops && npm run build
```
+3
View File
@@ -11,6 +11,8 @@ import { ReviewPage } from './pages/ReviewPage';
import { SummaryPage } from './pages/admin/SummaryPage';
import { UsersPage } from './pages/admin/UsersPage';
import { BillingPage } from './pages/admin/BillingPage';
import { WechatPage } from './pages/admin/WechatPage';
import { SystemPolicyPage } from './pages/admin/SystemPolicyPage';
export function App() {
return (
@@ -43,6 +45,7 @@ export function App() {
<Route path="users" element={<UsersPage />} />
<Route path="billing" element={<BillingPage />} />
<Route path="wechat" element={<WechatPage />} />
<Route path="policy" element={<SystemPolicyPage />} />
<Route path="*" element={<Navigate to="/admin" replace />} />
</Route>
+34
View File
@@ -177,12 +177,46 @@ export type CreateWechatNotificationPayload = {
channels?: Array<'web' | 'wechat'>;
};
export type SystemDisclosureMode = 'off' | 'shadow' | 'enforce';
export type SystemDisclosurePolicyConfig = {
enabled: boolean;
mode: SystemDisclosureMode;
refusalText: string;
productNames: string[];
selfReferences: string[];
categories: Record<string, boolean>;
};
export type SystemDisclosurePolicyState = {
config: SystemDisclosurePolicyConfig;
policyVersion: number;
updatedBy: string | null;
updatedAt: number | null;
source: string;
refreshedAt?: number | null;
lastRefreshError?: string | null;
};
// ─── Summary ──────────────────────────────────────────────────────────────────
export async function fetchAdminSummary() {
return adminFetch<{ summary: AdminSummary }>('/admin-api/summary');
}
// ─── Trust & Policy ───────────────────────────────────────────────────────────
export async function fetchSystemDisclosurePolicy() {
return adminFetch<SystemDisclosurePolicyState>('/admin-api/system-disclosure-policy/config');
}
export async function updateSystemDisclosurePolicy(config: SystemDisclosurePolicyConfig) {
return adminFetch<SystemDisclosurePolicyState>('/admin-api/system-disclosure-policy/config', {
method: 'PUT',
body: JSON.stringify({ config }),
});
}
// ─── Users ────────────────────────────────────────────────────────────────────
export async function fetchAdminUsers(params: {
+1
View File
@@ -5,6 +5,7 @@ const links = [
{ to: '/admin/users', label: '用户管理' },
{ to: '/admin/billing', label: '账单记录' },
{ to: '/admin/wechat', label: '服务号管理' },
{ to: '/admin/policy', label: '策略中心' },
];
export function AdminLayout() {
+235
View File
@@ -0,0 +1,235 @@
import { useEffect, useMemo, useState } from 'react';
import {
fetchSystemDisclosurePolicy,
updateSystemDisclosurePolicy,
type SystemDisclosurePolicyConfig,
type SystemDisclosurePolicyState,
} from '../../api/admin';
const CATEGORY_LABELS: Record<string, { title: string; description: string }> = {
architecture: {
title: '架构与内部实现',
description: '底层架构、运行机制、服务设计与内部实现细节。',
},
model_and_routing: {
title: '模型与路由',
description: '模型供应商、模型选择、意图路由与 Agent 编排。',
},
prompts_and_memory: {
title: '提示词与记忆',
description: '系统提示词、上下文拼接、记忆注入与召回机制。',
},
tools_and_extensions: {
title: '工具与扩展',
description: '内部工具、插件、Skill、MCP 清单和调用方式。',
},
data_and_deployment: {
title: '数据与部署',
description: '数据库结构、存储位置、部署拓扑、主机与端口。',
},
security_boundaries: {
title: '安全边界',
description: '沙箱、鉴权、权限策略、安全实现及绕过方式。',
},
};
function listToText(values: string[]) {
return values.join('\n');
}
function textToList(value: string) {
return [...new Set(value.split(/\r?\n|,/).map((item) => item.trim()).filter(Boolean))];
}
function formatTime(value?: number | null) {
return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '尚未保存';
}
export function SystemPolicyPage() {
const [state, setState] = useState<SystemDisclosurePolicyState | null>(null);
const [draft, setDraft] = useState<SystemDisclosurePolicyConfig | null>(null);
const [productNames, setProductNames] = useState('');
const [selfReferences, setSelfReferences] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const load = async () => {
setLoading(true);
setError(null);
try {
const result = await fetchSystemDisclosurePolicy();
setState(result);
setDraft(result.config);
setProductNames(listToText(result.config.productNames));
setSelfReferences(listToText(result.config.selfReferences));
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
void load();
}, []);
const dirty = useMemo(() => {
if (!state || !draft) return false;
return JSON.stringify({
...draft,
productNames: textToList(productNames),
selfReferences: textToList(selfReferences),
}) !== JSON.stringify(state.config);
}, [draft, productNames, selfReferences, state]);
const save = async () => {
if (!draft) return;
if (
draft.mode === 'enforce'
&& !window.confirm('确认进入强制执行模式?明确命中的请求将直接固定拒答,不再进入意图识别和模型。')
) {
return;
}
setSaving(true);
setError(null);
setNotice(null);
try {
const result = await updateSystemDisclosurePolicy({
...draft,
productNames: textToList(productNames),
selfReferences: textToList(selfReferences),
});
setState(result);
setDraft(result.config);
setProductNames(listToText(result.config.productNames));
setSelfReferences(listToText(result.config.selfReferences));
setNotice(`策略版本 v${result.policyVersion} 已保存。Portal 将通过内存快照自动刷新。`);
} catch (err) {
setError(err instanceof Error ? err.message : '保存失败');
} finally {
setSaving(false);
}
};
if (loading) return <p></p>;
if (!draft || !state) return <p className="alert">{error ?? '策略配置不可用'}</p>;
return (
<div className="grid">
<div className="card">
<h2 style={{ marginTop: 0 }}>Trust &amp; Policy Center</h2>
<p style={{ color: '#68716c', marginBottom: 0 }}>
System Disclosure Policy
</p>
</div>
{error ? <p className="alert">{error}</p> : null}
{notice ? <p style={{ color: '#2f6f57', fontSize: 13 }}>{notice}</p> : null}
<div className="card grid">
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(180px, 1fr) 2fr', gap: 16 }}>
<label>
<strong></strong>
<select
value={draft.mode}
onChange={(event) => setDraft({
...draft,
mode: event.target.value as SystemDisclosurePolicyConfig['mode'],
enabled: event.target.value !== 'off',
})}
style={{ marginTop: 8 }}
>
<option value="off"></option>
<option value="shadow">Shadow</option>
<option value="enforce">Enforce</option>
</select>
</label>
<div>
<strong>Zero-Impact Allow Invariant</strong>
<p style={{ color: '#68716c', margin: '8px 0 0' }}>
Allow Prompt
</p>
</div>
</div>
<div style={{ color: '#68716c', fontSize: 13 }}>
v{state.policyVersion} · {state.source} · {formatTime(state.updatedAt)}
</div>
</div>
<div className="card grid">
<h3 style={{ margin: 0 }}></h3>
{Object.entries(draft.categories).map(([key, enabled]) => {
const label = CATEGORY_LABELS[key] ?? { title: key, description: '' };
return (
<label
key={key}
style={{
display: 'grid',
gridTemplateColumns: 'auto 1fr',
alignItems: 'start',
gap: 10,
padding: '10px 0',
borderBottom: '1px solid #eee7da',
}}
>
<input
type="checkbox"
checked={enabled}
onChange={(event) => setDraft({
...draft,
categories: { ...draft.categories, [key]: event.target.checked },
})}
style={{ width: 'auto', marginTop: 3 }}
/>
<span>
<strong>{label.title}</strong>
<span style={{ display: 'block', color: '#68716c', fontSize: 13 }}>
{label.description}
</span>
</span>
</label>
);
})}
</div>
<div className="card grid">
<label>
<strong></strong>
<textarea
rows={4}
value={draft.refusalText}
onChange={(event) => setDraft({ ...draft, refusalText: event.target.value })}
style={{ marginTop: 8 }}
/>
</label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<label>
<strong></strong>
<span style={{ display: 'block', color: '#68716c', fontSize: 12, margin: '4px 0 8px' }}>
/ +
</span>
<textarea rows={8} value={productNames} onChange={(event) => setProductNames(event.target.value)} />
</label>
<label>
<strong></strong>
<span style={{ display: 'block', color: '#68716c', fontSize: 12, margin: '4px 0 8px' }}>
</span>
<textarea rows={8} value={selfReferences} onChange={(event) => setSelfReferences(event.target.value)} />
</label>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<button type="button" className="btn secondary" onClick={() => void load()} disabled={saving}>
</button>
<button type="button" className="btn" onClick={() => void save()} disabled={saving || !dirty}>
{saving ? '保存中…' : '保存新版本'}
</button>
</div>
</div>
);
}
+7
View File
@@ -214,6 +214,7 @@ import { createManagedMemoryV2Runtime } from './memory-v2-runtime.mjs';
import { createMemoryV2AdminConfigService } from './memory-v2-admin-config.mjs';
import { createEpisodicMemoryService } from './episodic-memory.mjs';
import { createSkillRuntimeAdminConfigService } from './skill-runtime-admin-config.mjs';
import { createSystemDisclosurePolicyService } from './system-disclosure-policy.mjs';
import { createWechatScheduleLlmConfigService } from './wechat-schedule-llm-config.mjs';
import { createExperienceService } from './experience-service.mjs';
import { attachAsrRoutes } from './asr-proxy.mjs';
@@ -384,6 +385,7 @@ let memoryV2 = null;
let episodicMemoryService = null;
let memoryV2ConfigService = null;
let skillRuntimeConfigService = null;
let systemDisclosurePolicyService = null;
let wechatScheduleLlmConfigService = null;
let mindSpace = null;
let mindSpaceAssets = null;
@@ -719,6 +721,8 @@ async function bootstrapUserAuth() {
});
memoryV2ConfigService = createMemoryV2AdminConfigService(pool);
skillRuntimeConfigService = createSkillRuntimeAdminConfigService(pool, { h5Root: __dirname });
systemDisclosurePolicyService = createSystemDisclosurePolicyService(pool);
await systemDisclosurePolicyService.initialize();
wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool);
conversationMemoryService = createConversationMemoryService(pool, {
llmProviderService,
@@ -782,6 +786,7 @@ async function bootstrapUserAuth() {
sessionSnapshotService,
conversationMemoryService,
memoryV2,
systemDisclosurePolicyService,
localFetchAsset: mindSpaceAssets
? async (userId, assetId) => {
const { asset, path: assetPath } = await mindSpaceAssets.readAsset(userId, assetId);
@@ -798,6 +803,7 @@ async function bootstrapUserAuth() {
tkmindProxy,
toolGateway,
directChatService,
systemDisclosurePolicyService,
chatIntentRouter,
sessionSnapshotService,
conversationMemoryService,
@@ -882,6 +888,7 @@ async function bootstrapUserAuth() {
wechatScheduleLlmConfigService,
llmProviderService,
chatIntentRouter,
systemDisclosurePolicyService,
sessionIntentClassifier: ({ text }) => chatIntentRouter?.classifySessionAction({ text }),
onPageGenerated: async ({ userId, sessionId, artifacts = [] }) => {
for (const artifact of artifacts) {
+412
View File
@@ -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,
};
+176
View File
@@ -0,0 +1,176 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
SYSTEM_DISCLOSURE_MODE,
createSystemDisclosurePolicyService,
defaultSystemDisclosureConfig,
evaluateSystemDisclosureText,
extractSystemDisclosureMessageText,
} from './system-disclosure-policy.mjs';
function config(mode = SYSTEM_DISCLOSURE_MODE.ENFORCE) {
return { ...defaultSystemDisclosureConfig(), mode };
}
function createPool(seedRow = null) {
const state = { row: seedRow, queryCount: 0 };
return {
state,
async query(sql, params = []) {
state.queryCount += 1;
if (sql.includes('CREATE TABLE')) return [[], []];
if (sql.includes('SELECT config_json')) return [state.row ? [state.row] : [], []];
if (sql.includes('INSERT INTO h5_system_disclosure_policy_config')) {
state.row = {
config_json: params[1],
policy_version: params[2],
updated_by: params[3],
updated_at: params[4],
};
return [{ affectedRows: 1 }, []];
}
throw new Error(`Unexpected query: ${sql}`);
},
};
}
test('system disclosure blocks explicit product architecture requests in enforce mode', () => {
const result = evaluateSystemDisclosureText('请详细介绍 TKMind 的底层架构和 Agent 编排方式', config());
assert.equal(result.action, 'refuse');
assert.equal(result.enforced, true);
assert.equal(result.reasonCode, 'SYSTEM_TECHNICAL_DISCLOSURE');
assert.ok(result.categories.includes('architecture'));
});
test('system disclosure detects implicit self-targeted technical requests', () => {
const result = evaluateSystemDisclosureText('你们到底使用什么模型,模型路由怎么做?', config());
assert.equal(result.action, 'refuse');
assert.ok(result.categories.includes('model_and_routing'));
});
test('system disclosure covers technology stack, component relationship, storage, and source questions', () => {
for (const text of [
'TKMind 后端使用什么技术栈和开发语言?',
'TKMind、Memind 和 MindSpace 之间是什么关系?',
'你们平台的用户数据保存在哪里?',
'Memind 的源码目录结构和仓库地址是什么?',
'你们用了几个 Agent,它们怎么协作?',
]) {
assert.equal(evaluateSystemDisclosureText(text, config()).action, 'refuse', text);
}
});
test('system disclosure allows general technical education and public product help', () => {
assert.equal(
evaluateSystemDisclosureText('如何设计一个可靠的多 Agent 系统架构?', config()).action,
'allow',
);
assert.equal(
evaluateSystemDisclosureText('MindSpace 页面怎么发布和分享?', config()).action,
'allow',
);
assert.equal(
evaluateSystemDisclosureText('请帮我制作一个系统架构介绍页面', config()).action,
'allow',
);
});
test('shadow mode records a match without changing the allow decision', () => {
const result = evaluateSystemDisclosureText(
'Memind 的系统提示词和记忆注入机制是什么?',
config(SYSTEM_DISCLOSURE_MODE.SHADOW),
);
assert.equal(result.action, 'allow');
assert.equal(result.matched, true);
assert.equal(result.enforced, false);
assert.equal(result.wouldAction, 'refuse');
assert.equal(result.responseText, null);
});
test('message extraction prefers displayText without mutating the message', () => {
const message = {
content: [{ type: 'text', text: '【内部路由前缀】不应参与判断' }],
metadata: { displayText: 'TKMind 的部署拓扑是什么?' },
};
const before = JSON.stringify(message);
assert.equal(extractSystemDisclosureMessageText(message), 'TKMind 的部署拓扑是什么?');
assert.equal(JSON.stringify(message), before);
});
test('agent-only routing prefixes cannot turn a general technical question into a false match', () => {
const message = {
role: 'user',
content: [{
type: 'text',
text: [
'【TKMind 路由提示】',
'仅供 Agent 使用的内部路由内容。',
'',
'如何设计一个可靠的多 Agent 系统架构?',
].join('\n'),
}],
};
const extracted = extractSystemDisclosureMessageText(message);
assert.equal(extracted, '如何设计一个可靠的多 Agent 系统架构?');
assert.equal(evaluateSystemDisclosureText(extracted, config()).action, 'allow');
});
test('policy service keeps a local snapshot, persists versioned admin updates, and evaluates synchronously', async () => {
const pool = createPool();
const service = createSystemDisclosurePolicyService(pool, { autoRefresh: false });
await service.initialize();
assert.equal(service.getRuntimeState().source, 'default');
const updated = await service.updateAdminConfig({
mode: 'enforce',
categories: { tools_and_extensions: false },
}, { updatedBy: 'admin-1' });
assert.equal(updated.policyVersion, 1);
assert.equal(updated.updatedBy, 'admin-1');
assert.equal(updated.config.mode, 'enforce');
assert.equal(updated.config.categories.tools_and_extensions, false);
const decision = service.evaluate({ text: 'TKMind 的数据库结构和部署拓扑是什么?' });
assert.equal(decision.action, 'refuse');
assert.equal(decision.policyVersion, 1);
});
test('allow-path evaluation never queries the database or mutates its input', async () => {
const pool = createPool();
const service = createSystemDisclosurePolicyService(pool, { autoRefresh: false });
await service.initialize();
const queryCountAfterInitialize = pool.state.queryCount;
const message = {
role: 'user',
content: [{ type: 'text', text: '请帮我安排明天的学习计划' }],
metadata: { displayText: '请帮我安排明天的学习计划' },
};
const before = JSON.stringify(message);
for (let index = 0; index < 100; index += 1) {
assert.equal(service.evaluate({ userMessage: message }).action, 'allow');
}
assert.equal(pool.state.queryCount, queryCountAfterInitialize);
assert.equal(JSON.stringify(message), before);
});
test('policy refresh failure preserves the last-known-good snapshot', async () => {
const pool = createPool({
config_json: JSON.stringify(config(SYSTEM_DISCLOSURE_MODE.ENFORCE)),
policy_version: 4,
updated_by: 'admin-1',
updated_at: 100,
});
const warnings = [];
const service = createSystemDisclosurePolicyService(pool, {
autoRefresh: false,
logger: { warn: (...args) => warnings.push(args) },
});
await service.initialize();
pool.query = async () => {
throw new Error('db unavailable');
};
await service.refresh();
assert.equal(service.getRuntimeState().policyVersion, 4);
assert.equal(service.evaluate({ text: 'TKMind 底层架构是什么?' }).action, 'refuse');
assert.equal(warnings.length, 1);
});
+28
View File
@@ -1103,6 +1103,7 @@ export function createTkmindProxy({
sessionSnapshotService,
conversationMemoryService,
memoryV2,
systemDisclosurePolicyService = null,
}) {
const sessionStore =
sessionAccess ?? createSessionAccess({ userAuth, enabled: isSessionBrokerEnabled() });
@@ -1645,6 +1646,33 @@ export function createTkmindProxy({
} = {},
) {
if (!userId || !sessionId) throw new Error('缺少会话信息');
let disclosureDecision = null;
try {
disclosureDecision = systemDisclosurePolicyService?.evaluate?.({
userMessage,
channel: 'goose_proxy',
userId,
sessionId,
}) ?? null;
} catch (err) {
console.warn(
'[TKMindProxy] system disclosure policy evaluation failed open:',
err instanceof Error ? err.message : err,
);
}
if (disclosureDecision?.enforced) {
const error = new Error(disclosureDecision.responseText || '不提供本系统内部技术信息');
error.code = 'SYSTEM_TECHNICAL_DISCLOSURE';
error.status = 403;
error.retryable = false;
error.policyDecision = {
policyId: disclosureDecision.policyId,
policyVersion: disclosureDecision.policyVersion,
reasonCode: disclosureDecision.reasonCode,
categories: disclosureDecision.categories,
};
throw error;
}
const owns = await sessionStore.validateOwnership(userId, sessionId);
if (!owns) throw new Error('无权访问该会话');
const gate = await userAuth.canUseChat(userId);
+87
View File
@@ -904,6 +904,93 @@ test('submitSessionReplyForUser adds goose metadata visibility flags before repl
});
});
test('submitSessionReplyForUser keeps an enforced disclosure policy as a final pre-model backstop', async () => {
await withFakeGoosedSession(async ({ apiTarget, workingDir, replyBodies }) => {
let ownershipChecks = 0;
const proxy = createTkmindProxy({
apiTarget,
apiSecret: 'test-secret',
systemDisclosurePolicyService: {
evaluate() {
return {
enforced: true,
policyId: 'system-disclosure',
policyVersion: 8,
reasonCode: 'SYSTEM_TECHNICAL_DISCLOSURE',
categories: ['architecture'],
responseText: '不提供内部技术信息。',
};
},
},
userAuth: {
...createMemoryTestUserAuth(workingDir),
async ownsSession() {
ownershipChecks += 1;
return true;
},
},
});
await assert.rejects(
proxy.submitSessionReplyForUser(
'user-1',
'session-1',
'request-policy-backstop',
{
role: 'user',
content: [{ type: 'text', text: 'TKMind 的底层架构是什么?' }],
},
),
(error) =>
error?.code === 'SYSTEM_TECHNICAL_DISCLOSURE'
&& error?.status === 403
&& error?.retryable === false,
);
assert.equal(ownershipChecks, 0);
assert.equal(replyBodies.length, 0);
});
});
test('submitSessionReplyForUser fails open when disclosure policy evaluation throws', async () => {
await withFakeGoosedSession(async ({ apiTarget, workingDir, replyBodies }) => {
const proxy = createTkmindProxy({
apiTarget,
apiSecret: 'test-secret',
systemDisclosurePolicyService: {
evaluate() {
throw new Error('policy unavailable');
},
},
userAuth: {
...createMemoryTestUserAuth(workingDir),
async ownsSession() {
return true;
},
async canUseChat() {
return { ok: true };
},
async getUserById() {
return { id: 'user-1' };
},
async resolveUserPolicies() {
return { unrestricted: true, policies: {} };
},
},
});
await proxy.submitSessionReplyForUser(
'user-1',
'session-1',
'request-policy-fail-open',
{
role: 'user',
content: [{ type: 'text', text: '普通聊天' }],
},
);
assert.equal(replyBodies.length, 1);
});
});
test('submitSessionReplyForUser fails closed when historical image scrub is unsupported', async () => {
await withFakeGoosedSession(async ({ apiTarget, workingDir, replyBodies }) => {
const proxy = createTkmindProxy({
+43
View File
@@ -1580,6 +1580,7 @@ export function createWechatMpService({
wechatScheduleLlmConfigService = null,
llmProviderService = null,
chatIntentRouter = null,
systemDisclosurePolicyService = null,
onPageGenerated = null,
applySessionLlmProvider = null,
refreshSessionSnapshot = null,
@@ -3177,6 +3178,48 @@ export function createWechatMpService({
await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash });
let disclosureDecision = null;
try {
disclosureDecision = systemDisclosurePolicyService?.evaluate?.({
text: intent.agentText,
channel: 'wechat_mp',
userId: boundUser.userId,
sessionId: null,
}) ?? null;
} catch (err) {
logger.warn?.(
'WeChat MP system disclosure policy evaluation failed open:',
err instanceof Error ? err.message : err,
);
}
if (disclosureDecision?.enforced) {
if (inbound.msgId && typeof userAuth.finishWechatMpMessage === 'function') {
await userAuth.finishWechatMpMessage({
appId: config.appId,
openid: inbound.fromUserName,
msgId: inbound.msgId,
status: 'done',
agentSessionId: null,
});
}
logger.info?.('[system-disclosure-policy] blocked WeChat request', {
userId: boundUser.userId,
policyVersion: disclosureDecision.policyVersion,
reasonCode: disclosureDecision.reasonCode,
categories: disclosureDecision.categories,
});
return {
ok: true,
status: 200,
contentType: 'application/xml; charset=utf-8',
body: buildWechatTextReply({
toUserName: inbound.fromUserName,
fromUserName: inbound.toUserName,
content: disclosureDecision.responseText,
}),
};
}
const scheduleReply =
intent.msgType === 'text' || intent.msgType === 'voice'
? await handleWechatScheduleIntent({
+61
View File
@@ -112,6 +112,7 @@ function createBoundWechatService({
applySessionLlmProvider = null,
submitSessionReply = null,
chatIntentRouter = null,
systemDisclosurePolicyService = null,
}) {
return createWechatMpService({
config: {
@@ -164,6 +165,7 @@ function createBoundWechatService({
sessionApiFetch,
submitSessionReply,
chatIntentRouter,
systemDisclosurePolicyService,
scheduleService,
applySessionLlmProvider,
wechatFetch,
@@ -171,6 +173,65 @@ function createBoundWechatService({
});
}
test('wechat system disclosure policy refuses before schedule, session, or model execution', async () => {
let sessionCalls = 0;
let scheduleCalls = 0;
const finished = [];
const service = createBoundWechatService({
systemDisclosurePolicyService: {
evaluate({ text, channel }) {
assert.equal(text, '请详细介绍 TKMind 的底层架构');
assert.equal(channel, 'wechat_mp');
return {
policyId: 'system-disclosure',
policyVersion: 3,
reasonCode: 'SYSTEM_TECHNICAL_DISCLOSURE',
categories: ['architecture'],
enforced: true,
responseText: '不提供内部技术信息。',
};
},
},
scheduleService: {
async listItems() {
scheduleCalls += 1;
return [];
},
},
sessionApiFetch: async () => {
sessionCalls += 1;
throw new Error('session path must not be called');
},
startAgentSession: async () => {
sessionCalls += 1;
throw new Error('session must not start');
},
userAuth: {
async finishWechatMpMessage(input) {
finished.push(input);
},
},
});
const timestamp = '1710000000';
const nonce = 'nonce-policy';
const result = await service.handleInboundMessage(
inboundXml({ content: '请详细介绍 TKMind 的底层架构' }),
{
timestamp,
nonce,
signature: signatureFor('token', timestamp, nonce),
},
);
assert.equal(result.status, 200);
assert.match(result.body, /不提供内部技术信息/);
assert.equal(sessionCalls, 0);
assert.equal(scheduleCalls, 0);
assert.equal(finished.length, 1);
assert.equal(finished[0].status, 'done');
});
test('splitWechatText respects WeChat 2048-byte customer service limit', () => {
const longChinese = '中'.repeat(900);
const chunks = splitWechatText(longChinese);