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:
+74
-13
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user