feat(mcp): 添加 CCW_DISABLE_SANDBOX 环境变量支持禁用工作空间访问限制

- 在 path-validator.ts 中添加 isSandboxDisabled() 函数
- 修改 validatePath() 在沙箱禁用时跳过路径限制检查
- MCP server 启动日志显示沙箱状态
- /api/mcp-install-ccw API 支持 disableSandbox 参数
- Dashboard UI 添加禁用沙箱的复选框选项
- 添加中英文 i18n 翻译支持
This commit is contained in:
catlog22
2026-01-20 11:50:23 +08:00
parent eea859dd6f
commit c1d12384c3
6 changed files with 69 additions and 11 deletions

View File

@@ -1171,13 +1171,17 @@ export async function handleMcpRoutes(ctx: RouteContext): Promise<boolean> {
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<string, any> = {
command: "cmd",
args: ["/c", "npx", "-y", "ccw-mcp"],
env: {
CCW_ENABLED_TOOLS: "all"
CCW_ENABLED_TOOLS: "all",
...(disableSandbox && { CCW_DISABLE_SANDBOX: "1" })
}
};

View File

@@ -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<void> {
// 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}`);
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`);

View File

@@ -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) {

View File

@@ -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 服务器',

View File

@@ -298,6 +298,14 @@ async function renderMcpManager() {
placeholder="${t('mcp.allowedDirsPlaceholder')}"
value="${getCcwAllowedDirsCodex()}">
</div>
<div class="flex items-center gap-2">
<label class="text-xs text-muted-foreground w-36 shrink-0">CCW_DISABLE_SANDBOX</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" class="ccw-disable-sandbox-checkbox-codex w-3 h-3"
${getCcwDisableSandboxCodex() ? 'checked' : ''}>
<span class="text-xs text-muted-foreground">${t('mcp.disableSandboxDesc')}</span>
</label>
</div>
</div>
</div>
</div>
@@ -512,6 +520,14 @@ async function renderMcpManager() {
placeholder="${t('mcp.allowedDirsPlaceholder')}"
value="${getCcwAllowedDirs()}">
</div>
<div class="flex items-center gap-2">
<label class="text-xs text-muted-foreground w-36 shrink-0">CCW_DISABLE_SANDBOX</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" class="ccw-disable-sandbox-checkbox w-3 h-3"
${getCcwDisableSandbox() ? 'checked' : ''}>
<span class="text-xs text-muted-foreground">${t('mcp.disableSandboxDesc')}</span>
</label>
</div>
</div>
</div>
</div>

View File

@@ -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<string> {
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`
);