611 lines
21 KiB
JavaScript
611 lines
21 KiB
JavaScript
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { resolveAgentGooseMode } from './policies.mjs';
|
||
|
||
/** Spawned as a separate Node process by goosed; must sit beside bundled portal runtime. */
|
||
export function resolveSandboxMcpServerPath(overridePath) {
|
||
const normalized = String(overridePath ?? '').trim();
|
||
if (normalized) return normalized;
|
||
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'mindspace-sandbox-mcp.mjs');
|
||
}
|
||
|
||
export function resolveSandboxMcpNodeExecPath(overridePath) {
|
||
const normalized = String(overridePath ?? '').trim();
|
||
if (normalized) return normalized;
|
||
return process.execPath;
|
||
}
|
||
|
||
const LOOPBACK_PG_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
||
|
||
export function resolveSandboxMcpUserDataPgUrl({
|
||
portalUrl,
|
||
mcpUrl,
|
||
containerized = false,
|
||
hostGateway = 'host.docker.internal',
|
||
} = {}) {
|
||
const explicitMcpUrl = String(mcpUrl ?? '').trim();
|
||
if (explicitMcpUrl) return explicitMcpUrl;
|
||
|
||
const sourceUrl = String(portalUrl ?? '').trim();
|
||
if (!sourceUrl || !containerized) return sourceUrl;
|
||
|
||
try {
|
||
const parsed = new URL(sourceUrl);
|
||
if (LOOPBACK_PG_HOSTS.has(parsed.hostname)) {
|
||
parsed.hostname = String(hostGateway || 'host.docker.internal').trim();
|
||
}
|
||
return parsed.toString();
|
||
} catch {
|
||
// Preserve the configured value so the MCP reports the real configuration
|
||
// error instead of silently falling back to its local Unix socket default.
|
||
return sourceUrl;
|
||
}
|
||
}
|
||
|
||
export function resolveMindSearchMcpServerPath(overridePath) {
|
||
const normalized = String(overridePath ?? '').trim();
|
||
if (normalized) return normalized;
|
||
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'tkmind-search-mcp.mjs');
|
||
}
|
||
|
||
export function resolveExcelMcpServerPath(overridePath) {
|
||
const normalized = String(overridePath ?? '').trim();
|
||
if (normalized) return normalized;
|
||
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'tkmind-excel-mcp.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>/ 内可使用 sandbox-fs 的 write_file/edit_file/read_file/create_dir(以及按权限开放的 list_dir;不可越出该目录)',
|
||
risk: 'medium',
|
||
category: 'publisher',
|
||
},
|
||
{
|
||
key: 'private_data_space',
|
||
label: '用户私有数据空间',
|
||
description:
|
||
'为用户提供唯一的私有 SQLite 数据空间,供 Agent 创建问卷、表单、清单等私有结构化数据表',
|
||
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: '会话级项目记忆注入(projectmemory,H5 引导用)',
|
||
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: 'web',
|
||
label: '网页搜索',
|
||
description: '联网搜索与抓取(platform/web:web_search、fetch_url)',
|
||
risk: 'medium',
|
||
category: 'knowledge',
|
||
},
|
||
{
|
||
key: 'search_external',
|
||
label: 'MindSearch 外部搜索增强',
|
||
description: '可插拔的外部搜索补充能力;不替代现有网页搜索,默认关闭',
|
||
risk: 'medium',
|
||
category: 'knowledge',
|
||
},
|
||
{
|
||
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',
|
||
},
|
||
{
|
||
key: 'openhands',
|
||
label: 'OpenHands 编码',
|
||
description: '复杂多文件编码与仓库级任务委托(openhands)',
|
||
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', 'apps']);
|
||
|
||
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,
|
||
private_data_space: true,
|
||
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,
|
||
web: true,
|
||
search_external: false,
|
||
computer: false,
|
||
charts: false,
|
||
aider: false,
|
||
openhands: 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 = [];
|
||
if (capabilities.static_publish) {
|
||
tools.push(
|
||
'read_file',
|
||
'write_file',
|
||
'edit_file',
|
||
'create_dir',
|
||
'generate_image',
|
||
'generate_docx',
|
||
'generate_long_image',
|
||
);
|
||
if (capabilities.shell || capabilities.code_browse) tools.push('list_dir');
|
||
}
|
||
if (capabilities.private_data_space) {
|
||
tools.push(
|
||
'private_data_info',
|
||
'private_data_schema',
|
||
'private_data_query',
|
||
'private_data_execute',
|
||
'private_data_register_dataset',
|
||
'private_data_set_page_policy',
|
||
'private_data_close_page_dataset',
|
||
'private_data_bind_workspace_page',
|
||
'schedule_create_item',
|
||
'schedule_create_reminder',
|
||
'schedule_list_items',
|
||
);
|
||
}
|
||
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;
|
||
}
|
||
|
||
function resolveSandboxMcpLocalRoot(sandboxMcp) {
|
||
const localRoot = sandboxMcp?.workspaceRoot || sandboxMcp?.sandboxRoot || '';
|
||
return localRoot ? String(localRoot) : '';
|
||
}
|
||
|
||
function resolveSandboxMcpCompatRoot(sandboxMcp) {
|
||
const compatRoot = sandboxMcp?.sandboxRoot || sandboxMcp?.workspaceRoot || '';
|
||
return compatRoot ? String(compatRoot) : '';
|
||
}
|
||
|
||
function sandboxMcpEnvs(sandboxMcp, mcpTools) {
|
||
const localRoot = resolveSandboxMcpLocalRoot(sandboxMcp);
|
||
const compatRoot = resolveSandboxMcpCompatRoot(sandboxMcp);
|
||
const envs = {
|
||
ALLOWED_TOOLS: mcpTools.join(','),
|
||
};
|
||
if (compatRoot) envs.SANDBOX_ROOT = compatRoot;
|
||
if (sandboxMcp.workspaceRoot || localRoot) envs.MINDSPACE_WORKSPACE_ROOT = sandboxMcp.workspaceRoot || localRoot;
|
||
if (sandboxMcp.workspaceRef) envs.MINDSPACE_WORKSPACE_REF = sandboxMcp.workspaceRef;
|
||
if (sandboxMcp.userId) envs.PRIVATE_DATA_USER_ID = sandboxMcp.userId;
|
||
if (mcpTools.includes('generate_image')) {
|
||
if (sandboxMcp.agentApiBaseUrl) {
|
||
envs.MINDSPACE_AGENT_API_BASE_URL = sandboxMcp.agentApiBaseUrl;
|
||
}
|
||
if (sandboxMcp.internalAgentSecret) {
|
||
envs.MINDSPACE_INTERNAL_AGENT_SECRET = sandboxMcp.internalAgentSecret;
|
||
}
|
||
}
|
||
if (mcpTools.includes('private_data_info')) {
|
||
const userDataPgUrl = resolveSandboxMcpUserDataPgUrl({
|
||
portalUrl: sandboxMcp.userDataPgUrl ?? process.env.MINDSPACE_USERDATA_PG_URL,
|
||
mcpUrl: sandboxMcp.userDataMcpPgUrl ?? process.env.MINDSPACE_USERDATA_MCP_PG_URL,
|
||
containerized: Boolean(sandboxMcp.containerized),
|
||
hostGateway:
|
||
sandboxMcp.userDataPgHostGateway ?? process.env.MINDSPACE_USERDATA_MCP_PG_HOST ?? 'host.docker.internal',
|
||
});
|
||
envs.MINDSPACE_USERDATA_BACKEND = sandboxMcp.userDataBackend ?? process.env.MINDSPACE_USERDATA_BACKEND ?? 'postgres';
|
||
if (userDataPgUrl) envs.MINDSPACE_USERDATA_PG_URL = userDataPgUrl;
|
||
const autoProvision = sandboxMcp.userDataAutoProvision ?? process.env.MINDSPACE_USERDATA_AUTO_PROVISION;
|
||
if (autoProvision != null && String(autoProvision).trim()) {
|
||
envs.MINDSPACE_USERDATA_AUTO_PROVISION = String(autoProvision).trim();
|
||
}
|
||
}
|
||
for (const key of [
|
||
'DATABASE_URL',
|
||
'MYSQL_HOST',
|
||
'MYSQL_PORT',
|
||
'MYSQL_USER',
|
||
'MYSQL_PASSWORD',
|
||
'MYSQL_DATABASE',
|
||
'PRIVATE_DATA_MAX_BYTES',
|
||
]) {
|
||
if (process.env[key]) envs[key] = process.env[key];
|
||
}
|
||
return envs;
|
||
}
|
||
|
||
/**
|
||
* Build goose agent/start extension_overrides from resolved capability flags.
|
||
* Returns null when the caller should use server defaults (admin / unrestricted).
|
||
*
|
||
* sandboxMcp: { serverPath, sandboxRoot?, workspaceRoot?, workspaceRef?, nodeExecPath? }
|
||
* `sandboxRoot` remains the legacy local-path compatibility field.
|
||
* `workspaceRoot` is the preferred local adapter field for future MindSpace runtime extraction.
|
||
* When a local root is available 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, toolMode = 'chat', mindSearchConfig = null } = {},
|
||
) {
|
||
if (unrestricted) {
|
||
return { extensionOverrides: null, enableContextMemory: true, gooseMode: 'auto' };
|
||
}
|
||
|
||
const extensions = [];
|
||
if (capabilities.static_publish || (capabilities.private_data_space && sandboxMcp)) {
|
||
const localRoot = resolveSandboxMcpLocalRoot(sandboxMcp);
|
||
const compatRoot = resolveSandboxMcpCompatRoot(sandboxMcp);
|
||
if (sandboxMcp?.serverPath && localRoot) {
|
||
// 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:
|
||
'工作区沙箱文件系统与用户私有数据空间。用户私有数据空间是当前用户隔离的 PostgreSQL schema,适合问卷、表单、清单、调研数据和分析中间表;不要用于账号、计费、权限、审计、公开平台数据或跨用户数据。',
|
||
display_name: 'sandbox-fs',
|
||
bundled: false,
|
||
cmd: resolveSandboxMcpNodeExecPath(sandboxMcp.nodeExecPath),
|
||
// Legacy MCP still accepts a local filesystem root as argv[2] even when the source
|
||
// capability is named workspaceRoot instead of sandboxRoot.
|
||
args: [sandboxMcp.serverPath, compatRoot],
|
||
// envs (goosed field name) as belt-and-suspenders backup
|
||
envs: sandboxMcpEnvs(sandboxMcp, mcpTools),
|
||
available_tools: mcpTools,
|
||
});
|
||
}
|
||
// Keep developer extension only for image reading when applicable.
|
||
if (capabilities.image_read) {
|
||
extensions.push(makeExtension('platform', 'developer', ['read_image']));
|
||
}
|
||
} else if (capabilities.static_publish) {
|
||
// 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));
|
||
}
|
||
}
|
||
if (capabilities.static_publish) {
|
||
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));
|
||
}
|
||
}
|
||
const enableCodeExecutionExtension = /^(1|true|yes)$/i.test(
|
||
process.env.TKMIND_ENABLE_CODE_EXECUTION_EXTENSION ?? '',
|
||
);
|
||
if (capabilities.code_sandbox && enableCodeExecutionExtension) {
|
||
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.web) {
|
||
extensions.push(makeExtension('platform', 'web', ['web_search', 'fetch_url']));
|
||
}
|
||
const searchEnabled = capabilities.search_external && mindSearchConfig?.enabled && mindSearchConfig.mode !== 'off';
|
||
if (searchEnabled && (!policies || policies.network_egress !== 'deny')) {
|
||
const hasResearchService = mindSearchConfig.services?.some((service) =>
|
||
service.enabled && service.id === mindSearchConfig.routes?.research && service.adapter === 'research-http');
|
||
extensions.push({
|
||
type: 'stdio',
|
||
name: 'tkmind-search',
|
||
description: 'MindSearch 外部搜索增强(补充能力)',
|
||
display_name: 'tkmind-search',
|
||
bundled: false,
|
||
cmd: resolveSandboxMcpNodeExecPath(process.env.GOOSED_MCP_NODE_PATH),
|
||
args: [resolveMindSearchMcpServerPath(process.env.TKMIND_SEARCH_MCP_SERVER_PATH)],
|
||
envs: {
|
||
TKMIND_SEARCH_ENABLED: '1',
|
||
TKMIND_SEARCH_MODE: mindSearchConfig.mode,
|
||
TKMIND_SEARCH_PROVIDER_SEARXNG_ENABLED: mindSearchConfig.providers?.searxng ? '1' : '0',
|
||
TKMIND_SEARCH_PROVIDER_GITHUB_ENABLED: mindSearchConfig.providers?.github ? '1' : '0',
|
||
TKMIND_SEARCH_READER_ENABLED: mindSearchConfig.providers?.reader ? '1' : '0',
|
||
TKMIND_SEARCH_SEARXNG_URL: mindSearchConfig.settings?.searxngEndpoint ?? '',
|
||
TKMIND_SEARCH_MAX_RESULTS: String(mindSearchConfig.settings?.maxResults ?? 10),
|
||
TKMIND_SEARCH_TIMEOUT_MS: String(mindSearchConfig.settings?.timeoutMs ?? 8000),
|
||
TKMIND_SEARCH_READER_MAX_CHARS: String(mindSearchConfig.settings?.readerMaxChars ?? 12000),
|
||
TKMIND_SEARCH_SERVICES_JSON: JSON.stringify(mindSearchConfig.services ?? []),
|
||
TKMIND_SEARCH_ROUTES_JSON: JSON.stringify(mindSearchConfig.routes ?? {}),
|
||
},
|
||
available_tools: ['tkmind_search', 'tkmind_read', ...(hasResearchService ? ['tkmind_research'] : [])],
|
||
});
|
||
}
|
||
if (capabilities.excel_analysis) {
|
||
const excelWorkspaceRoot = resolveSandboxMcpLocalRoot(sandboxMcp);
|
||
if (excelWorkspaceRoot) {
|
||
extensions.push({
|
||
type: 'stdio',
|
||
name: 'tkmind-excel',
|
||
description: 'Excel Analyst:当前用户工作区内 .xlsx 的只读检查、统计分析、图表与一致性校验报告。',
|
||
display_name: 'Excel Analyst',
|
||
bundled: false,
|
||
cmd: resolveSandboxMcpNodeExecPath(sandboxMcp?.nodeExecPath),
|
||
args: [resolveExcelMcpServerPath(process.env.GOOSED_EXCEL_MCP_SERVER_PATH), excelWorkspaceRoot],
|
||
envs: {
|
||
EXCEL_ANALYST_ENABLED: '1',
|
||
MINDSPACE_WORKSPACE_ROOT: excelWorkspaceRoot,
|
||
...(sandboxMcp?.workspaceRef ? { MINDSPACE_WORKSPACE_REF: sandboxMcp.workspaceRef } : {}),
|
||
},
|
||
available_tools: ['excel_inspect', 'excel_analyze', 'excel_chart', 'excel_report'],
|
||
});
|
||
}
|
||
}
|
||
if (capabilities.computer) {
|
||
extensions.push(makeExtension('builtin', 'computercontroller', []));
|
||
}
|
||
if (capabilities.charts) {
|
||
extensions.push(makeExtension('builtin', 'autovisualiser', []));
|
||
}
|
||
const codeToolMode = toolMode === 'code' || toolMode === 'code-task';
|
||
if (codeToolMode && capabilities.aider) {
|
||
extensions.push({
|
||
...makeExtension('platform', 'aider', []),
|
||
timeout_ms: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000),
|
||
metadata: { runtime_scope: 'code_tool_task' },
|
||
});
|
||
}
|
||
if (codeToolMode && capabilities.openhands) {
|
||
extensions.push({
|
||
...makeExtension('platform', 'openhands', []),
|
||
timeout_ms: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000),
|
||
metadata: { runtime_scope: 'code_tool_task' },
|
||
});
|
||
}
|
||
|
||
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;
|
||
}
|