Files
memind/capabilities.mjs
T
Your Name b0f5d6a51c Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 16:39:39 -07:00

370 lines
11 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.
import { resolveAgentGooseMode } from './policies.mjs';
export const CAPABILITY_CATALOG = [
{
key: 'shell',
label: 'Shell 命令',
description: '执行 bash/shell 命令(developer.shell',
risk: 'high',
category: 'developer',
},
{
key: 'static_publish',
label: '用户沙箱目录',
description:
'在 MindSpace/<用户ID>/ 内可使用 write、edit、shell、tree(不可越出该目录;由 static-page-publish 技能开通)',
risk: 'medium',
category: 'publisher',
},
{
key: 'filesystem',
label: '文件读写(全目录)',
description: '在工作区内任意读写文件(developer.write / edit),仅高级用户',
risk: 'high',
category: 'developer',
},
{
key: 'code_browse',
label: '代码浏览',
description: '目录树、代码结构分析(developer.tree、analyze',
risk: 'low',
category: 'developer',
},
{
key: 'image_read',
label: '图片读取',
description: '读取本地或网络图片(developer.read_image',
risk: 'low',
category: 'developer',
},
{
key: 'skills',
label: '技能加载',
description: '加载技能与知识(skills / summon.load',
risk: 'low',
category: 'knowledge',
},
{
key: 'subagent',
label: '子任务派发',
description: 'delegate 子 Agent 执行复杂任务(summon',
risk: 'high',
category: 'agent',
},
{
key: 'code_sandbox',
label: '沙箱脚本',
description: 'execute_typescript 在沙箱中批量调用工具(code_execution',
risk: 'high',
category: 'agent',
},
{
key: 'chat_recall',
label: '对话回溯',
description: '检索历史会话(chatrecall',
risk: 'low',
category: 'memory',
},
{
key: 'context_memory',
label: '项目记忆',
description: '会话级项目记忆注入(projectmemoryH5 引导用)',
risk: 'low',
category: 'memory',
},
{
key: 'memory_store',
label: '长期记忆',
description: '记住/检索用户偏好(memory 扩展)',
risk: 'medium',
category: 'memory',
},
{
key: 'extension_admin',
label: '扩展管理',
description: '搜索、启用、禁用扩展(extensionmanager',
risk: 'high',
category: 'admin',
},
{
key: 'apps',
label: '应用管理',
description: '创建与管理 TKMind 应用(apps',
risk: 'medium',
category: 'agent',
},
{
key: 'todo',
label: '待办事项',
description: '任务列表跟踪(todo',
risk: 'low',
category: 'productivity',
},
{
key: 'computer',
label: '电脑控制',
description: '自动化脚本、网页抓取、文档处理(computercontroller',
risk: 'high',
category: 'automation',
},
{
key: 'charts',
label: '数据可视化',
description: '图表与可视化(autovisualiser',
risk: 'low',
category: 'automation',
},
{
key: 'aider',
label: 'Aider 编码',
description: '多文件编码委托(aider',
risk: 'high',
category: 'developer',
},
];
/** Capabilities that must never be enabled for regular users, even via DB overrides. */
export const USER_NON_GRANTABLE_CAPABILITIES = new Set(['extension_admin']);
export function clampUserCapabilities(capabilities) {
const clamped = { ...capabilities };
for (const key of USER_NON_GRANTABLE_CAPABILITIES) {
clamped[key] = false;
}
return clamped;
}
export const DEFAULT_USER_CAPABILITIES = Object.fromEntries(
CAPABILITY_CATALOG.map(({ key }) => {
const defaults = {
shell: false,
static_publish: false,
filesystem: false,
code_browse: false,
image_read: true,
skills: true,
subagent: false,
code_sandbox: false,
chat_recall: true,
context_memory: true,
memory_store: true,
extension_admin: false,
apps: false,
todo: false,
computer: false,
charts: false,
aider: false,
};
return [key, defaults[key] ?? false];
}),
);
const CATALOG_KEYS = new Set(CAPABILITY_CATALOG.map((item) => item.key));
export function catalogKeys() {
return [...CATALOG_KEYS];
}
export function isValidCapabilityKey(key) {
return CATALOG_KEYS.has(key);
}
function makeExtension(type, name, tools = []) {
return {
type,
name,
description: '',
display_name: name,
bundled: true,
available_tools: tools,
};
}
export const SANDBOX_DEVELOPER_TOOLS = ['write', 'edit', 'shell', 'tree', 'read_image'];
/** static_publish sandbox: only file writes inside MindSpace/<userId>/ — no tree/shell by default */
export function sandboxDeveloperTools(capabilities) {
const tools = ['write', 'edit'];
if (capabilities.shell) {
tools.push('shell', 'tree');
} else if (capabilities.code_browse) {
tools.push('tree');
}
if (capabilities.image_read) tools.push('read_image');
return tools;
}
/**
* Tools exposed by the sandbox MCP server (mindspace-sandbox-mcp.mjs) for static_publish users.
* read_file is always included because edit_file requires reading first.
*/
export function sandboxMcpTools(capabilities) {
const tools = ['read_file', 'write_file', 'edit_file', 'create_dir'];
if (capabilities.shell || capabilities.code_browse) tools.push('list_dir');
return tools;
}
export function developerToolsFromPolicy(sessionPolicy) {
const developer = sessionPolicy?.extensionOverrides?.find((ext) => ext.name === 'developer');
const sandboxFs = sessionPolicy?.extensionOverrides?.find((ext) => ext.name === 'sandbox-fs');
return (sandboxFs ?? developer)?.available_tools ?? [];
}
function mergeDeveloperTools(capabilities) {
const tools = [];
if (capabilities.shell) tools.push('shell');
if (capabilities.filesystem) tools.push('write', 'edit');
if (capabilities.code_browse) tools.push('tree');
if (capabilities.image_read) tools.push('read_image');
return tools;
}
/**
* Build goose agent/start extension_overrides from resolved capability flags.
* Returns null when the caller should use server defaults (admin / unrestricted).
*
* sandboxMcp: { serverPath, sandboxRoot, nodeExecPath? }
* When provided for a static_publish user, the built-in developer extension is replaced
* by a sandboxed stdio MCP that enforces filesystem boundaries at the OS level.
*/
export function buildAgentExtensionPolicy(
capabilities,
{ unrestricted = false, policies = null, sandboxMcp = null } = {},
) {
if (unrestricted) {
return { extensionOverrides: null, enableContextMemory: true, gooseMode: 'auto' };
}
const extensions = [];
if (capabilities.static_publish) {
if (sandboxMcp?.serverPath && sandboxMcp?.sandboxRoot) {
// Sandboxed stdio MCP: enforces SANDBOX_ROOT at the OS level.
// Replaces the built-in developer extension so path traversal is impossible.
const mcpTools = sandboxMcpTools(capabilities);
if (mcpTools.length > 0) {
extensions.push({
type: 'stdio',
name: 'sandbox-fs',
description: '工作区沙箱文件系统(路径限制在用户工作区内)',
display_name: 'sandbox-fs',
bundled: false,
cmd: sandboxMcp.nodeExecPath ?? process.execPath,
// sandboxRoot passed as argv[2] so it works even if goosed doesn't forward envs
args: [sandboxMcp.serverPath, sandboxMcp.sandboxRoot],
// envs (goosed field name) as belt-and-suspenders backup
envs: {
SANDBOX_ROOT: sandboxMcp.sandboxRoot,
ALLOWED_TOOLS: mcpTools.join(','),
},
available_tools: mcpTools,
});
}
// Keep developer extension only for image reading when applicable.
if (capabilities.image_read) {
extensions.push(makeExtension('platform', 'developer', ['read_image']));
}
} else {
// Fallback when sandbox MCP is not configured: use built-in developer extension.
// This is the legacy path — file operations are NOT boundary-enforced.
const sandboxTools = sandboxDeveloperTools(capabilities);
if (sandboxTools.length > 0) {
extensions.push(makeExtension('platform', 'developer', sandboxTools));
}
}
extensions.push(makeExtension('platform', 'skills', []));
extensions.push(makeExtension('platform', 'summon', ['load_skill']));
extensions.push(makeExtension('platform', 'projectmemory', []));
} else {
const developerTools = mergeDeveloperTools(capabilities);
if (developerTools.length > 0) {
extensions.push(makeExtension('platform', 'developer', developerTools));
}
if (capabilities.code_browse) {
extensions.push(makeExtension('platform', 'analyze', []));
}
if (capabilities.skills) {
extensions.push(makeExtension('platform', 'skills', []));
}
if (capabilities.skills || capabilities.subagent) {
const summonTools = [];
if (capabilities.skills) summonTools.push('load', 'load_skill');
if (capabilities.subagent) summonTools.push('delegate');
extensions.push(makeExtension('platform', 'summon', summonTools));
}
}
if (capabilities.code_sandbox) {
extensions.push(makeExtension('platform', 'code_execution', []));
}
if (capabilities.chat_recall) {
extensions.push(makeExtension('platform', 'chatrecall', []));
}
if (capabilities.context_memory) {
extensions.push(makeExtension('platform', 'projectmemory', []));
}
if (capabilities.memory_store) {
extensions.push(makeExtension('builtin', 'memory', []));
}
if (capabilities.extension_admin) {
extensions.push(makeExtension('platform', 'extensionmanager', []));
}
if (capabilities.apps) {
extensions.push(makeExtension('platform', 'apps', []));
}
if (capabilities.todo) {
extensions.push(makeExtension('platform', 'todo', []));
}
if (capabilities.computer) {
extensions.push(makeExtension('builtin', 'computercontroller', []));
}
if (capabilities.charts) {
extensions.push(makeExtension('builtin', 'autovisualiser', []));
}
if (capabilities.aider) {
extensions.push(makeExtension('platform', 'aider', []));
}
return {
extensionOverrides: extensions,
enableContextMemory: Boolean(
capabilities.context_memory || capabilities.chat_recall || capabilities.static_publish,
),
gooseMode: resolveAgentGooseMode(capabilities, policies),
};
}
/**
* Narrow policy for in-preview page edit sub-sessions: patch API via shell when allowed,
* otherwise reply-only patches via mindspace-page-update blocks.
*/
export function buildPageEditAgentPolicy(basePolicy) {
if (basePolicy?.unrestricted) {
return {
...basePolicy,
enableContextMemory: false,
gooseMode: 'auto',
};
}
const baseDeveloper = basePolicy?.extensionOverrides?.find((ext) => ext.name === 'developer');
const canShell =
baseDeveloper?.available_tools?.includes('shell') ||
basePolicy?.capabilities?.shell === true;
const extensions = canShell ? [makeExtension('platform', 'developer', ['shell'])] : [];
return {
...basePolicy,
extensionOverrides: extensions,
enableContextMemory: false,
gooseMode: 'auto',
};
}
export function normalizeCapabilityPatch(patch) {
const normalized = {};
for (const [key, value] of Object.entries(patch ?? {})) {
if (!isValidCapabilityKey(key)) continue;
normalized[key] = Boolean(value);
}
return normalized;
}