Files
memind/policies.mjs
john 14a00774d9 feat(h5): web 联网能力、实时查询路由与 session Finish 对齐
- 新增 web 能力并挂载 platform/web(web_search/fetch_url)
- 实时查询强制 web skill,router fallback 与 await session Finish
- Session Broker 覆盖率/指标、stream replay 与相关单测/E2E 脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 16:06:26 +08:00

254 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** @typedef {'auto' | 'approve' | 'smart_approve' | 'chat'} GooseMode */
/** @typedef {'readwrite' | 'readonly'} WorkspaceAccess */
/** @typedef {'allow' | 'deny'} NetworkEgress */
export const POLICY_CATALOG = [
{
key: 'goose_mode',
label: 'TKMind 模式',
description:
'H5 终端用户不在聊天里点「允许/拒绝」;可用工具由「能力权限」决定。chat 不调用工具,auto 在授权范围内自动执行。',
type: 'select',
options: [
{ value: 'chat', label: '仅聊天 (chat)' },
{ value: 'auto', label: '自动执行 (auto)' },
],
defaultValue: 'chat',
category: 'goose',
risk: 'high',
},
{
key: 'workspace_access',
label: '工作区访问',
description: 'readonly 时禁止 shell 与文件写入,仅允许浏览与分析',
type: 'select',
options: [
{ value: 'readwrite', label: '读写' },
{ value: 'readonly', label: '只读' },
],
defaultValue: 'readwrite',
category: 'workspace',
risk: 'medium',
},
{
key: 'network_egress',
label: '网络出站',
description: 'deny 时禁用 shell、电脑控制、沙箱脚本等可能访问网络的扩展',
type: 'select',
options: [
{ value: 'deny', label: '禁止(推荐)' },
{ value: 'allow', label: '允许(需同时开启对应能力)' },
],
defaultValue: 'deny',
category: 'network',
risk: 'high',
},
{
key: 'api_lockdown',
label: 'API 代理锁定',
description: '开启后仅允许聊天相关 API,拦截改模式、加扩展、写配置等危险请求',
type: 'boolean',
defaultValue: true,
category: 'proxy',
risk: 'medium',
},
{
key: 'code_delegate_executor',
label: '代码委托执行器',
description:
'控制 Goose 在多文件编码、修复与重构任务中优先委托给谁。auto 由 Goose 结合当前可用扩展自行判断;aider / openhands 则优先使用指定执行器。',
type: 'select',
options: [
{ value: 'auto', label: '自动选择 (auto)' },
{ value: 'aider', label: '优先 Aider' },
{ value: 'openhands', label: '优先 OpenHands' },
],
defaultValue: 'auto',
category: 'routing',
risk: 'medium',
},
{
key: 'code_task_routing',
label: '代码任务路由策略',
description:
'定义代码任务如何在 Aider 与 OpenHands 之间分流。balanced 由 Goose 按任务复杂度判断;split 建议小改动走 Aider、复杂任务走 OpenHandsforce_* 则强制优先单一路径。',
type: 'select',
options: [
{ value: 'balanced', label: '平衡路由 (balanced)' },
{ value: 'split', label: '小改动 Aider,复杂任务 OpenHands' },
{ value: 'force_aider', label: '尽量统一走 Aider' },
{ value: 'force_openhands', label: '尽量统一走 OpenHands' },
],
defaultValue: 'balanced',
category: 'routing',
risk: 'medium',
},
];
export const DEFAULT_USER_POLICIES = Object.fromEntries(
POLICY_CATALOG.map((item) => [item.key, item.defaultValue]),
);
const POLICY_KEYS = new Set(POLICY_CATALOG.map((item) => item.key));
const GOOSE_MODES = new Set(['auto', 'approve', 'smart_approve', 'chat']);
const WORKSPACE_ACCESS = new Set(['readwrite', 'readonly']);
const NETWORK_EGRESS = new Set(['allow', 'deny']);
const CODE_DELEGATE_EXECUTORS = new Set(['auto', 'aider', 'openhands']);
const CODE_TASK_ROUTINGS = new Set(['balanced', 'split', 'force_aider', 'force_openhands']);
export function policyKeys() {
return [...POLICY_KEYS];
}
export function isValidPolicyKey(key) {
return POLICY_KEYS.has(key);
}
export function normalizePolicyPatch(patch) {
const normalized = {};
for (const [key, raw] of Object.entries(patch ?? {})) {
if (!isValidPolicyKey(key)) continue;
const def = POLICY_CATALOG.find((item) => item.key === key);
if (!def) continue;
if (def.type === 'boolean') {
normalized[key] = raw === true || raw === 'true' || raw === 1 || raw === '1';
continue;
}
const value = String(raw ?? '').trim();
if (def.key === 'goose_mode' && GOOSE_MODES.has(value)) normalized[key] = value;
if (def.key === 'workspace_access' && WORKSPACE_ACCESS.has(value)) normalized[key] = value;
if (def.key === 'network_egress' && NETWORK_EGRESS.has(value)) normalized[key] = value;
if (def.key === 'code_delegate_executor' && CODE_DELEGATE_EXECUTORS.has(value)) {
normalized[key] = value;
}
if (def.key === 'code_task_routing' && CODE_TASK_ROUTINGS.has(value)) {
normalized[key] = value;
}
}
return normalized;
}
export function resolvePolicies(rolePolicies, userOverrides) {
const resolved = { ...DEFAULT_USER_POLICIES };
for (const [key, value] of Object.entries(rolePolicies ?? {})) {
if (isValidPolicyKey(key)) resolved[key] = value;
}
for (const [key, value] of Object.entries(userOverrides ?? {})) {
if (isValidPolicyKey(key)) resolved[key] = value;
}
if (resolved.goose_mode === 'approve' || resolved.goose_mode === 'smart_approve') {
resolved.goose_mode = 'auto';
}
return resolved;
}
function hasExecutableTools(capabilities) {
return Boolean(
capabilities?.static_publish
|| capabilities?.shell
|| capabilities?.filesystem
|| capabilities?.code_browse
|| capabilities?.subagent
|| capabilities?.code_sandbox
|| capabilities?.computer
|| capabilities?.charts
|| capabilities?.aider
|| capabilities?.openhands
|| capabilities?.apps
|| capabilities?.todo
|| capabilities?.skills
|| capabilities?.chat_recall,
);
}
/**
* Goose mode for H5 agent sessions. Tool allow/deny is enforced via capability grants,
* not end-user confirmation dialogs.
*/
export function resolveAgentGooseMode(capabilities, policies) {
const requested = policies?.goose_mode ?? DEFAULT_USER_POLICIES.goose_mode;
if (!hasExecutableTools(capabilities)) {
return 'chat';
}
if (requested === 'chat' || requested === 'approve' || requested === 'smart_approve') {
return 'auto';
}
return 'auto';
}
/**
* Apply security policies on top of capability flags before building extension overrides.
*/
export function applyPoliciesToCapabilities(capabilities, policies) {
const effective = { ...capabilities };
if (policies.workspace_access === 'readonly') {
effective.shell = false;
effective.filesystem = false;
effective.static_publish = false;
effective.aider = false;
effective.openhands = false;
effective.code_sandbox = false;
effective.image_read = false;
}
if (policies.network_egress === 'deny') {
// static_publish 用户的 shell 仅能在本工作区内执行本地命令,不依赖外网出站
if (!effective.static_publish) {
effective.shell = false;
}
effective.computer = false;
effective.web = false;
effective.code_sandbox = false;
}
return effective;
}
const USER_API_ALLOWLIST = [
{ method: 'GET', pattern: /^\/status$/ },
{ method: 'POST', pattern: /^\/agent\/start$/ },
{ method: 'POST', pattern: /^\/agent\/resume$/ },
{ method: 'POST', pattern: /^\/agent\/runs$/ },
{ method: 'GET', pattern: /^\/agent\/runs\/[^/]+$/ },
{ method: 'GET', pattern: /^\/agent\/runs\/[^/]+\/events$/ },
{ method: 'GET', pattern: /^\/sessions$/ },
{ method: 'GET', pattern: /^\/sessions\/[^/]+$/ },
{ method: 'DELETE', pattern: /^\/sessions\/[^/]+$/ },
{ method: 'GET', pattern: /^\/sessions\/[^/]+\/events$/ },
{ method: 'POST', pattern: /^\/sessions\/[^/]+\/reply$/ },
{ method: 'POST', pattern: /^\/sessions\/[^/]+\/cancel$/ },
{ method: 'POST', pattern: /^\/action-required\/tool-confirmation$/ },
{ method: 'POST', pattern: /^\/agent\/update_provider$/ },
{ method: 'POST', pattern: /^\/config\/read$/ },
{ method: 'POST', pattern: /^\/agent\/harness_bootstrap$/ },
{ method: 'POST', pattern: /^\/agent\/harness_remember$/ },
];
export function isNativeH5ApiPath(pathname) {
const path = String(pathname ?? '');
return path.startsWith('/mindspace/') || path.startsWith('/plaza/');
}
/**
* @returns {{ allowed: boolean, reason?: string }}
*/
export function evaluateProxyRequest(method, pathname, policies, { unrestricted = false } = {}) {
if (unrestricted) return { allowed: true };
if (!policies.api_lockdown) return { allowed: true };
if (isNativeH5ApiPath(pathname)) {
return {
allowed: false,
reason: `H5 本地接口不应走 Agent 代理:${method.toUpperCase()} ${pathname}`,
};
}
const upper = method.toUpperCase();
const allowed = USER_API_ALLOWLIST.some(
(rule) => rule.method === upper && rule.pattern.test(pathname),
);
if (allowed) return { allowed: true };
return {
allowed: false,
reason: `策略已锁定 API${upper} ${pathname}`,
};
}