mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
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:
@@ -1171,13 +1171,17 @@ export async function handleMcpRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
return { error: 'projectPath is required', status: 400 };
|
return { error: 'projectPath is required', status: 400 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if sandbox should be disabled
|
||||||
|
const disableSandbox = body.disableSandbox === true;
|
||||||
|
|
||||||
// Generate CCW MCP server config
|
// Generate CCW MCP server config
|
||||||
// Use cmd /c to inherit Claude Code's working directory
|
// Use cmd /c to inherit Claude Code's working directory
|
||||||
const ccwMcpConfig = {
|
const ccwMcpConfig: Record<string, any> = {
|
||||||
command: "cmd",
|
command: "cmd",
|
||||||
args: ["/c", "npx", "-y", "ccw-mcp"],
|
args: ["/c", "npx", "-y", "ccw-mcp"],
|
||||||
env: {
|
env: {
|
||||||
CCW_ENABLED_TOOLS: "all"
|
CCW_ENABLED_TOOLS: "all",
|
||||||
|
...(disableSandbox && { CCW_DISABLE_SANDBOX: "1" })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { getAllToolSchemas, executeTool, executeToolWithProgress } from '../tools/index.js';
|
import { getAllToolSchemas, executeTool, executeToolWithProgress } from '../tools/index.js';
|
||||||
import type { ToolSchema, ToolResult } from '../types/tool.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_NAME = 'ccw-tools';
|
||||||
const SERVER_VERSION = '6.2.0';
|
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)
|
// Log server start (to stderr to not interfere with stdio protocol)
|
||||||
const projectRoot = getProjectRoot();
|
const projectRoot = getProjectRoot();
|
||||||
const allowedDirs = getAllowedDirectories();
|
const allowedDirs = getAllowedDirectories();
|
||||||
|
const sandboxDisabled = isSandboxDisabled();
|
||||||
console.error(`${SERVER_NAME} v${SERVER_VERSION} started`);
|
console.error(`${SERVER_NAME} v${SERVER_VERSION} started`);
|
||||||
console.error(`Project root: ${projectRoot}`);
|
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]) {
|
if (!process.env[ENV_PROJECT_ROOT]) {
|
||||||
console.error(`[Warning] ${ENV_PROJECT_ROOT} not set, using process.cwd()`);
|
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`);
|
console.error(`[Tip] Set ${ENV_PROJECT_ROOT} in your MCP config to specify project directory`);
|
||||||
|
|||||||
@@ -1085,12 +1085,27 @@ function selectCcwTools(type) {
|
|||||||
function getCcwPathConfig() {
|
function getCcwPathConfig() {
|
||||||
const projectRootInput = document.querySelector('.ccw-project-root-input');
|
const projectRootInput = document.querySelector('.ccw-project-root-input');
|
||||||
const allowedDirsInput = document.querySelector('.ccw-allowed-dirs-input');
|
const allowedDirsInput = document.querySelector('.ccw-allowed-dirs-input');
|
||||||
|
const disableSandboxCheckbox = document.querySelector('.ccw-disable-sandbox-checkbox');
|
||||||
return {
|
return {
|
||||||
projectRoot: projectRootInput?.value || '',
|
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
|
// Set CCW_PROJECT_ROOT to current project path
|
||||||
function setCcwProjectRootToCurrent() {
|
function setCcwProjectRootToCurrent() {
|
||||||
const input = document.querySelector('.ccw-project-root-input');
|
const input = document.querySelector('.ccw-project-root-input');
|
||||||
@@ -1102,7 +1117,7 @@ function setCcwProjectRootToCurrent() {
|
|||||||
// Build CCW Tools config with selected tools
|
// Build CCW Tools config with selected tools
|
||||||
// Uses globally installed ccw-mcp command (from claude-code-workflow package)
|
// Uses globally installed ccw-mcp command (from claude-code-workflow package)
|
||||||
function buildCcwToolsConfig(selectedTools, pathConfig = {}) {
|
function buildCcwToolsConfig(selectedTools, pathConfig = {}) {
|
||||||
const { projectRoot, allowedDirs } = pathConfig;
|
const { projectRoot, allowedDirs, disableSandbox } = pathConfig;
|
||||||
// Use globally installed ccw-mcp command directly
|
// Use globally installed ccw-mcp command directly
|
||||||
// Requires: npm install -g claude-code-workflow
|
// Requires: npm install -g claude-code-workflow
|
||||||
const config = {
|
const config = {
|
||||||
@@ -1134,6 +1149,10 @@ function buildCcwToolsConfig(selectedTools, pathConfig = {}) {
|
|||||||
if (allowedDirs && allowedDirs.trim()) {
|
if (allowedDirs && allowedDirs.trim()) {
|
||||||
config.env.CCW_ALLOWED_DIRS = 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
|
// Remove env object if empty
|
||||||
if (config.env && Object.keys(config.env).length === 0) {
|
if (config.env && Object.keys(config.env).length === 0) {
|
||||||
|
|||||||
@@ -972,6 +972,7 @@ const i18n = {
|
|||||||
'mcp.useCurrentDir': 'Use current directory',
|
'mcp.useCurrentDir': 'Use current directory',
|
||||||
'mcp.useCurrentProject': 'Use current project',
|
'mcp.useCurrentProject': 'Use current project',
|
||||||
'mcp.allowedDirsPlaceholder': 'Comma-separated paths (optional)',
|
'mcp.allowedDirsPlaceholder': 'Comma-separated paths (optional)',
|
||||||
|
'mcp.disableSandboxDesc': 'Allow access to any directory',
|
||||||
|
|
||||||
// Codex MCP
|
// Codex MCP
|
||||||
'mcp.codex.globalServers': 'Codex Global MCP Servers',
|
'mcp.codex.globalServers': 'Codex Global MCP Servers',
|
||||||
@@ -3299,6 +3300,7 @@ const i18n = {
|
|||||||
'mcp.useCurrentDir': '使用当前目录',
|
'mcp.useCurrentDir': '使用当前目录',
|
||||||
'mcp.useCurrentProject': '使用当前项目',
|
'mcp.useCurrentProject': '使用当前项目',
|
||||||
'mcp.allowedDirsPlaceholder': '逗号分隔的路径列表(可选)',
|
'mcp.allowedDirsPlaceholder': '逗号分隔的路径列表(可选)',
|
||||||
|
'mcp.disableSandboxDesc': '允许访问任意目录',
|
||||||
|
|
||||||
// Codex MCP
|
// Codex MCP
|
||||||
'mcp.codex.globalServers': 'Codex 全局 MCP 服务器',
|
'mcp.codex.globalServers': 'Codex 全局 MCP 服务器',
|
||||||
|
|||||||
@@ -298,6 +298,14 @@ async function renderMcpManager() {
|
|||||||
placeholder="${t('mcp.allowedDirsPlaceholder')}"
|
placeholder="${t('mcp.allowedDirsPlaceholder')}"
|
||||||
value="${getCcwAllowedDirsCodex()}">
|
value="${getCcwAllowedDirsCodex()}">
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -512,6 +520,14 @@ async function renderMcpManager() {
|
|||||||
placeholder="${t('mcp.allowedDirsPlaceholder')}"
|
placeholder="${t('mcp.allowedDirsPlaceholder')}"
|
||||||
value="${getCcwAllowedDirs()}">
|
value="${getCcwAllowedDirs()}">
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ import { constants } from 'fs';
|
|||||||
// Environment variable configuration
|
// Environment variable configuration
|
||||||
const ENV_PROJECT_ROOT = 'CCW_PROJECT_ROOT';
|
const ENV_PROJECT_ROOT = 'CCW_PROJECT_ROOT';
|
||||||
const ENV_ALLOWED_DIRS = 'CCW_ALLOWED_DIRS';
|
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
|
* Get project root directory
|
||||||
@@ -100,6 +110,7 @@ export async function validatePath(
|
|||||||
mustExist?: boolean;
|
mustExist?: boolean;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
const sandboxDisabled = isSandboxDisabled();
|
||||||
const allowedDirs = options.allowedDirectories || getAllowedDirectories();
|
const allowedDirs = options.allowedDirectories || getAllowedDirectories();
|
||||||
|
|
||||||
// 1. Resolve to absolute path
|
// 1. Resolve to absolute path
|
||||||
@@ -108,8 +119,8 @@ export async function validatePath(
|
|||||||
: resolve(getProjectRoot(), filePath);
|
: resolve(getProjectRoot(), filePath);
|
||||||
const normalizedPath = normalizePath(absolutePath);
|
const normalizedPath = normalizePath(absolutePath);
|
||||||
|
|
||||||
// 2. Initial sandbox check
|
// 2. Initial sandbox check (skip if sandbox is disabled)
|
||||||
if (!isPathWithinAllowedDirectories(normalizedPath, allowedDirs)) {
|
if (!sandboxDisabled && !isPathWithinAllowedDirectories(normalizedPath, allowedDirs)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Access denied: path "${normalizedPath}" is outside allowed directories. ` +
|
`Access denied: path "${normalizedPath}" is outside allowed directories. ` +
|
||||||
`Allowed: [${allowedDirs.join(', ')}]`
|
`Allowed: [${allowedDirs.join(', ')}]`
|
||||||
@@ -121,7 +132,8 @@ export async function validatePath(
|
|||||||
const realPath = await realpath(absolutePath);
|
const realPath = await realpath(absolutePath);
|
||||||
const normalizedReal = normalizePath(realPath);
|
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(
|
throw new Error(
|
||||||
`Access denied: symlink target "${normalizedReal}" is outside allowed directories`
|
`Access denied: symlink target "${normalizedReal}" is outside allowed directories`
|
||||||
);
|
);
|
||||||
@@ -135,13 +147,13 @@ export async function validatePath(
|
|||||||
throw new Error(`File not found: ${absolutePath}`);
|
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, '..');
|
const parentDir = resolve(absolutePath, '..');
|
||||||
try {
|
try {
|
||||||
const realParent = await realpath(parentDir);
|
const realParent = await realpath(parentDir);
|
||||||
const normalizedParent = normalizePath(realParent);
|
const normalizedParent = normalizePath(realParent);
|
||||||
|
|
||||||
if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirs)) {
|
if (!sandboxDisabled && !isPathWithinAllowedDirectories(normalizedParent, allowedDirs)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Access denied: parent directory "${normalizedParent}" is outside allowed directories`
|
`Access denied: parent directory "${normalizedParent}" is outside allowed directories`
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user