Files
memind/policies.mjs
T
John 6ee6fd64dd Add MindSpace page live edit, chat skills, and H5 deploy tooling.
Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 22:09:38 -07:00

209 lines
6.8 KiB
JavaScript
Raw 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',
},
];
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']);
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;
}
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?.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.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.code_sandbox = false;
}
return effective;
}
const USER_API_ALLOWLIST = [
{ method: 'GET', pattern: /^\/status$/ },
{ method: 'POST', pattern: /^\/agent\/start$/ },
{ method: 'POST', pattern: /^\/agent\/resume$/ },
{ 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}`,
};
}