Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+207
@@ -0,0 +1,207 @@
|
||||
/** @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: '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}`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user