Fix sandbox MCP path resolution for session policy sync.

Resolve mindspace-sandbox-mcp.mjs from the code module location instead of h5Root, and copy it into the portal runtime artifact so goosed can spawn sandbox-fs on production.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-26 13:36:05 +08:00
parent d51df2fb0a
commit 9ed4fd48d7
4 changed files with 761 additions and 57 deletions
+74 -13
View File
@@ -1,5 +1,12 @@
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() {
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'mindspace-sandbox-mcp.mjs');
}
export const CAPABILITY_CATALOG = [
{
key: 'shell',
@@ -12,7 +19,15 @@ export const CAPABILITY_CATALOG = [
key: 'static_publish',
label: '用户沙箱目录',
description:
'在 MindSpace/<用户ID>/ 内可使用 write、edit、shell、tree(不可越出该目录;由 static-page-publish 技能开通',
'在 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',
},
@@ -121,6 +136,13 @@ export const CAPABILITY_CATALOG = [
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. */
@@ -139,6 +161,7 @@ export const DEFAULT_USER_CAPABILITIES = Object.fromEntries(
const defaults = {
shell: false,
static_publish: false,
private_data_space: true,
filesystem: false,
code_browse: false,
image_read: true,
@@ -154,6 +177,7 @@ export const DEFAULT_USER_CAPABILITIES = Object.fromEntries(
computer: false,
charts: false,
aider: false,
openhands: false,
};
return [key, defaults[key] ?? false];
}),
@@ -199,8 +223,22 @@ export function sandboxDeveloperTools(capabilities) {
* 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');
const tools = [];
if (capabilities.static_publish) {
tools.push('read_file', 'write_file', 'edit_file', 'create_dir');
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',
'schedule_create_item',
'schedule_create_reminder',
'schedule_list_items',
);
}
return tools;
}
@@ -219,6 +257,26 @@ function mergeDeveloperTools(capabilities) {
return tools;
}
function sandboxMcpEnvs(sandboxMcp, mcpTools) {
const envs = {
SANDBOX_ROOT: sandboxMcp.sandboxRoot,
ALLOWED_TOOLS: mcpTools.join(','),
};
if (sandboxMcp.userId) envs.PRIVATE_DATA_USER_ID = sandboxMcp.userId;
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).
@@ -236,7 +294,7 @@ export function buildAgentExtensionPolicy(
}
const extensions = [];
if (capabilities.static_publish) {
if (capabilities.static_publish || (capabilities.private_data_space && sandboxMcp)) {
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.
@@ -245,17 +303,15 @@ export function buildAgentExtensionPolicy(
extensions.push({
type: 'stdio',
name: 'sandbox-fs',
description: '工作区沙箱文件系统(路径限制在用户工作区内)',
description:
'工作区沙箱文件系统与用户私有数据空间。用户私有数据空间是当前用户唯一的 SQLite 数据库,适合问卷、表单、清单、调研数据和分析中间表;不要用于账号、计费、权限、审计、公开平台数据或跨用户数据。',
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(','),
},
envs: sandboxMcpEnvs(sandboxMcp, mcpTools),
available_tools: mcpTools,
});
}
@@ -263,7 +319,7 @@ export function buildAgentExtensionPolicy(
if (capabilities.image_read) {
extensions.push(makeExtension('platform', 'developer', ['read_image']));
}
} else {
} 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);
@@ -271,9 +327,11 @@ export function buildAgentExtensionPolicy(
extensions.push(makeExtension('platform', 'developer', sandboxTools));
}
}
extensions.push(makeExtension('platform', 'skills', []));
extensions.push(makeExtension('platform', 'summon', ['load_skill']));
extensions.push(makeExtension('platform', 'projectmemory', []));
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) {
@@ -322,6 +380,9 @@ export function buildAgentExtensionPolicy(
if (capabilities.aider) {
extensions.push(makeExtension('platform', 'aider', []));
}
if (capabilities.openhands) {
extensions.push(makeExtension('platform', 'openhands', []));
}
return {
extensionOverrides: extensions,