From c1d12384c3b14aba09397642cfbf26c99e9235ac Mon Sep 17 00:00:00 2001 From: catlog22 Date: Tue, 20 Jan 2026 11:50:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20=E6=B7=BB=E5=8A=A0=20CCW=5FDISABLE?= =?UTF-8?q?=5FSANDBOX=20=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=A6=81=E7=94=A8=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 path-validator.ts 中添加 isSandboxDisabled() 函数 - 修改 validatePath() 在沙箱禁用时跳过路径限制检查 - MCP server 启动日志显示沙箱状态 - /api/mcp-install-ccw API 支持 disableSandbox 参数 - Dashboard UI 添加禁用沙箱的复选框选项 - 添加中英文 i18n 翻译支持 --- ccw/src/core/routes/mcp-routes.ts | 8 +++++-- ccw/src/mcp-server/index.ts | 9 ++++++-- .../dashboard-js/components/mcp-manager.js | 23 +++++++++++++++++-- ccw/src/templates/dashboard-js/i18n.js | 2 ++ .../dashboard-js/views/mcp-manager.js | 16 +++++++++++++ ccw/src/utils/path-validator.ts | 22 ++++++++++++++---- 6 files changed, 69 insertions(+), 11 deletions(-) diff --git a/ccw/src/core/routes/mcp-routes.ts b/ccw/src/core/routes/mcp-routes.ts index bbb553ba..2a587021 100644 --- a/ccw/src/core/routes/mcp-routes.ts +++ b/ccw/src/core/routes/mcp-routes.ts @@ -1171,13 +1171,17 @@ export async function handleMcpRoutes(ctx: RouteContext): Promise { return { error: 'projectPath is required', status: 400 }; } + // Check if sandbox should be disabled + const disableSandbox = body.disableSandbox === true; + // Generate CCW MCP server config // Use cmd /c to inherit Claude Code's working directory - const ccwMcpConfig = { + const ccwMcpConfig: Record = { command: "cmd", args: ["/c", "npx", "-y", "ccw-mcp"], env: { - CCW_ENABLED_TOOLS: "all" + CCW_ENABLED_TOOLS: "all", + ...(disableSandbox && { CCW_DISABLE_SANDBOX: "1" }) } }; diff --git a/ccw/src/mcp-server/index.ts b/ccw/src/mcp-server/index.ts index 4abb17e9..8346aa71 100644 --- a/ccw/src/mcp-server/index.ts +++ b/ccw/src/mcp-server/index.ts @@ -12,7 +12,7 @@ import { } from '@modelcontextprotocol/sdk/types.js'; import { getAllToolSchemas, executeTool, executeToolWithProgress } from '../tools/index.js'; import type { ToolSchema, ToolResult } from '../types/tool.js'; -import { getProjectRoot, getAllowedDirectories } from '../utils/path-validator.js'; +import { getProjectRoot, getAllowedDirectories, isSandboxDisabled } from '../utils/path-validator.js'; const SERVER_NAME = 'ccw-tools'; const SERVER_VERSION = '6.2.0'; @@ -169,9 +169,14 @@ async function main(): Promise { // Log server start (to stderr to not interfere with stdio protocol) const projectRoot = getProjectRoot(); const allowedDirs = getAllowedDirectories(); + const sandboxDisabled = isSandboxDisabled(); console.error(`${SERVER_NAME} v${SERVER_VERSION} started`); console.error(`Project root: ${projectRoot}`); - console.error(`Allowed directories: ${allowedDirs.join(', ')}`); + if (sandboxDisabled) { + console.error(`Sandbox: DISABLED (CCW_DISABLE_SANDBOX=true)`); + } else { + console.error(`Allowed directories: ${allowedDirs.join(', ')}`); + } if (!process.env[ENV_PROJECT_ROOT]) { console.error(`[Warning] ${ENV_PROJECT_ROOT} not set, using process.cwd()`); console.error(`[Tip] Set ${ENV_PROJECT_ROOT} in your MCP config to specify project directory`); diff --git a/ccw/src/templates/dashboard-js/components/mcp-manager.js b/ccw/src/templates/dashboard-js/components/mcp-manager.js index 49bc9bb4..0eae4046 100644 --- a/ccw/src/templates/dashboard-js/components/mcp-manager.js +++ b/ccw/src/templates/dashboard-js/components/mcp-manager.js @@ -1085,12 +1085,27 @@ function selectCcwTools(type) { function getCcwPathConfig() { const projectRootInput = document.querySelector('.ccw-project-root-input'); const allowedDirsInput = document.querySelector('.ccw-allowed-dirs-input'); + const disableSandboxCheckbox = document.querySelector('.ccw-disable-sandbox-checkbox'); return { projectRoot: projectRootInput?.value || '', - allowedDirs: allowedDirsInput?.value || '' + allowedDirs: allowedDirsInput?.value || '', + disableSandbox: disableSandboxCheckbox?.checked || false }; } +// Get CCW_DISABLE_SANDBOX checkbox status for Claude Code mode +function getCcwDisableSandbox() { + // Check if already installed and has the setting + const ccwToolsConfig = projectMcpServers?.['ccw-tools'] || globalServers?.['ccw-tools']; + return ccwToolsConfig?.env?.CCW_DISABLE_SANDBOX === '1' || ccwToolsConfig?.env?.CCW_DISABLE_SANDBOX === 'true'; +} + +// Get CCW_DISABLE_SANDBOX checkbox status for Codex mode +function getCcwDisableSandboxCodex() { + const ccwToolsConfig = codexMcpServers?.['ccw-tools']; + return ccwToolsConfig?.env?.CCW_DISABLE_SANDBOX === '1' || ccwToolsConfig?.env?.CCW_DISABLE_SANDBOX === 'true'; +} + // Set CCW_PROJECT_ROOT to current project path function setCcwProjectRootToCurrent() { const input = document.querySelector('.ccw-project-root-input'); @@ -1102,7 +1117,7 @@ function setCcwProjectRootToCurrent() { // Build CCW Tools config with selected tools // Uses globally installed ccw-mcp command (from claude-code-workflow package) function buildCcwToolsConfig(selectedTools, pathConfig = {}) { - const { projectRoot, allowedDirs } = pathConfig; + const { projectRoot, allowedDirs, disableSandbox } = pathConfig; // Use globally installed ccw-mcp command directly // Requires: npm install -g claude-code-workflow const config = { @@ -1134,6 +1149,10 @@ function buildCcwToolsConfig(selectedTools, pathConfig = {}) { if (allowedDirs && allowedDirs.trim()) { config.env.CCW_ALLOWED_DIRS = allowedDirs.trim(); } + // Add sandbox disable option + if (disableSandbox) { + config.env.CCW_DISABLE_SANDBOX = '1'; + } // Remove env object if empty if (config.env && Object.keys(config.env).length === 0) { diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index bb042441..196906f7 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -972,6 +972,7 @@ const i18n = { 'mcp.useCurrentDir': 'Use current directory', 'mcp.useCurrentProject': 'Use current project', 'mcp.allowedDirsPlaceholder': 'Comma-separated paths (optional)', + 'mcp.disableSandboxDesc': 'Allow access to any directory', // Codex MCP 'mcp.codex.globalServers': 'Codex Global MCP Servers', @@ -3299,6 +3300,7 @@ const i18n = { 'mcp.useCurrentDir': '使用当前目录', 'mcp.useCurrentProject': '使用当前项目', 'mcp.allowedDirsPlaceholder': '逗号分隔的路径列表(可选)', + 'mcp.disableSandboxDesc': '允许访问任意目录', // Codex MCP 'mcp.codex.globalServers': 'Codex 全局 MCP 服务器', diff --git a/ccw/src/templates/dashboard-js/views/mcp-manager.js b/ccw/src/templates/dashboard-js/views/mcp-manager.js index 594cc1c1..9e190cc9 100644 --- a/ccw/src/templates/dashboard-js/views/mcp-manager.js +++ b/ccw/src/templates/dashboard-js/views/mcp-manager.js @@ -298,6 +298,14 @@ async function renderMcpManager() { placeholder="${t('mcp.allowedDirsPlaceholder')}" value="${getCcwAllowedDirsCodex()}"> +
+ + +
@@ -512,6 +520,14 @@ async function renderMcpManager() { placeholder="${t('mcp.allowedDirsPlaceholder')}" value="${getCcwAllowedDirs()}"> +
+ + +
diff --git a/ccw/src/utils/path-validator.ts b/ccw/src/utils/path-validator.ts index 14ac5e0f..42d0bf3f 100644 --- a/ccw/src/utils/path-validator.ts +++ b/ccw/src/utils/path-validator.ts @@ -14,6 +14,16 @@ import { constants } from 'fs'; // Environment variable configuration const ENV_PROJECT_ROOT = 'CCW_PROJECT_ROOT'; const ENV_ALLOWED_DIRS = 'CCW_ALLOWED_DIRS'; +const ENV_DISABLE_SANDBOX = 'CCW_DISABLE_SANDBOX'; + +/** + * Check if sandbox mode is disabled + * When disabled, path validation allows access to any directory + */ +export function isSandboxDisabled(): boolean { + const value = process.env[ENV_DISABLE_SANDBOX]; + return value === '1' || value?.toLowerCase() === 'true'; +} /** * Get project root directory @@ -100,6 +110,7 @@ export async function validatePath( mustExist?: boolean; } = {} ): Promise { + const sandboxDisabled = isSandboxDisabled(); const allowedDirs = options.allowedDirectories || getAllowedDirectories(); // 1. Resolve to absolute path @@ -108,8 +119,8 @@ export async function validatePath( : resolve(getProjectRoot(), filePath); const normalizedPath = normalizePath(absolutePath); - // 2. Initial sandbox check - if (!isPathWithinAllowedDirectories(normalizedPath, allowedDirs)) { + // 2. Initial sandbox check (skip if sandbox is disabled) + if (!sandboxDisabled && !isPathWithinAllowedDirectories(normalizedPath, allowedDirs)) { throw new Error( `Access denied: path "${normalizedPath}" is outside allowed directories. ` + `Allowed: [${allowedDirs.join(', ')}]` @@ -121,7 +132,8 @@ export async function validatePath( const realPath = await realpath(absolutePath); const normalizedReal = normalizePath(realPath); - if (!isPathWithinAllowedDirectories(normalizedReal, allowedDirs)) { + // Skip sandbox check for symlink target if sandbox is disabled + if (!sandboxDisabled && !isPathWithinAllowedDirectories(normalizedReal, allowedDirs)) { throw new Error( `Access denied: symlink target "${normalizedReal}" is outside allowed directories` ); @@ -135,13 +147,13 @@ export async function validatePath( throw new Error(`File not found: ${absolutePath}`); } - // Validate parent directory's real path + // Validate parent directory's real path (skip if sandbox is disabled) const parentDir = resolve(absolutePath, '..'); try { const realParent = await realpath(parentDir); const normalizedParent = normalizePath(realParent); - if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirs)) { + if (!sandboxDisabled && !isPathWithinAllowedDirectories(normalizedParent, allowedDirs)) { throw new Error( `Access denied: parent directory "${normalizedParent}" is outside allowed directories` );