Compare commits

...

3 Commits

5 changed files with 47 additions and 29 deletions

View File

@@ -70,7 +70,7 @@ import mcpUtilsRoutes from './routes/mcp-utils.js';
import commandsRoutes from './routes/commands.js'; import commandsRoutes from './routes/commands.js';
import settingsRoutes from './routes/settings.js'; import settingsRoutes from './routes/settings.js';
import agentRoutes from './routes/agent.js'; import agentRoutes from './routes/agent.js';
import projectsRoutes, { FORBIDDEN_PATHS } from './routes/projects.js'; import projectsRoutes, { WORKSPACES_ROOT, validateWorkspacePath } from './routes/projects.js';
import cliAuthRoutes from './routes/cli-auth.js'; import cliAuthRoutes from './routes/cli-auth.js';
import userRoutes from './routes/user.js'; import userRoutes from './routes/user.js';
import codexRoutes from './routes/codex.js'; import codexRoutes from './routes/codex.js';
@@ -484,22 +484,42 @@ app.post('/api/projects/create', authenticateToken, async (req, res) => {
} }
}); });
const expandWorkspacePath = (inputPath) => {
if (!inputPath) return inputPath;
if (inputPath === '~') {
return WORKSPACES_ROOT;
}
if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) {
return path.join(WORKSPACES_ROOT, inputPath.slice(2));
}
return inputPath;
};
// Browse filesystem endpoint for project suggestions - uses existing getFileTree // Browse filesystem endpoint for project suggestions - uses existing getFileTree
app.get('/api/browse-filesystem', authenticateToken, async (req, res) => { app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
try { try {
const { path: dirPath } = req.query; const { path: dirPath } = req.query;
console.log('[API] Browse filesystem request for path:', dirPath);
console.log('[API] WORKSPACES_ROOT is:', WORKSPACES_ROOT);
// Default to home directory if no path provided // Default to home directory if no path provided
const homeDir = os.homedir(); const defaultRoot = WORKSPACES_ROOT;
let targetPath = dirPath ? dirPath.replace('~', homeDir) : homeDir; let targetPath = dirPath ? expandWorkspacePath(dirPath) : defaultRoot;
// Resolve and normalize the path // Resolve and normalize the path
targetPath = path.resolve(targetPath); targetPath = path.resolve(targetPath);
// Security check - ensure path is within allowed workspace root
const validation = await validateWorkspacePath(targetPath);
if (!validation.valid) {
return res.status(403).json({ error: validation.error });
}
const resolvedPath = validation.resolvedPath || targetPath;
// Security check - ensure path is accessible // Security check - ensure path is accessible
try { try {
await fs.promises.access(targetPath); await fs.promises.access(resolvedPath);
const stats = await fs.promises.stat(targetPath); const stats = await fs.promises.stat(resolvedPath);
if (!stats.isDirectory()) { if (!stats.isDirectory()) {
return res.status(400).json({ error: 'Path is not a directory' }); return res.status(400).json({ error: 'Path is not a directory' });
@@ -509,7 +529,7 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
} }
// Use existing getFileTree function with shallow depth (only direct children) // Use existing getFileTree function with shallow depth (only direct children)
const fileTree = await getFileTree(targetPath, 1, 0, false); // maxDepth=1, showHidden=false const fileTree = await getFileTree(resolvedPath, 1, 0, false); // maxDepth=1, showHidden=false
// Filter only directories and format for suggestions // Filter only directories and format for suggestions
const directories = fileTree const directories = fileTree
@@ -529,7 +549,13 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
// Add common directories if browsing home directory // Add common directories if browsing home directory
const suggestions = []; const suggestions = [];
if (targetPath === homeDir) { let resolvedWorkspaceRoot = defaultRoot;
try {
resolvedWorkspaceRoot = await fsPromises.realpath(defaultRoot);
} catch (error) {
// Use default root as-is if realpath fails
}
if (resolvedPath === resolvedWorkspaceRoot) {
const commonDirs = ['Desktop', 'Documents', 'Projects', 'Development', 'Dev', 'Code', 'workspace']; const commonDirs = ['Desktop', 'Documents', 'Projects', 'Development', 'Dev', 'Code', 'workspace'];
const existingCommon = directories.filter(dir => commonDirs.includes(dir.name)); const existingCommon = directories.filter(dir => commonDirs.includes(dir.name));
const otherDirs = directories.filter(dir => !commonDirs.includes(dir.name)); const otherDirs = directories.filter(dir => !commonDirs.includes(dir.name));
@@ -540,7 +566,7 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
} }
res.json({ res.json({
path: targetPath, path: resolvedPath,
suggestions: suggestions suggestions: suggestions
}); });
@@ -556,22 +582,13 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => {
if (!folderPath) { if (!folderPath) {
return res.status(400).json({ error: 'Path is required' }); return res.status(400).json({ error: 'Path is required' });
} }
const homeDir = os.homedir(); const expandedPath = expandWorkspacePath(folderPath);
const targetPath = path.resolve(folderPath.replace('~', homeDir)); const resolvedInput = path.resolve(expandedPath);
const normalizedPath = path.normalize(targetPath); const validation = await validateWorkspacePath(resolvedInput);
const comparePath = normalizedPath.toLowerCase(); if (!validation.valid) {
const forbiddenLower = FORBIDDEN_PATHS.map(p => p.toLowerCase()); return res.status(403).json({ error: validation.error });
if (forbiddenLower.includes(comparePath) || comparePath === '/') {
return res.status(403).json({ error: 'Cannot create folders in system directories' });
}
for (const forbidden of forbiddenLower) {
if (comparePath.startsWith(forbidden + path.sep)) {
if (forbidden === '/var' && (comparePath.startsWith('/var/tmp') || comparePath.startsWith('/var/folders'))) {
continue;
}
return res.status(403).json({ error: `Cannot create folders in system directory: ${forbidden}` });
}
} }
const targetPath = validation.resolvedPath || resolvedInput;
const parentDir = path.dirname(targetPath); const parentDir = path.dirname(targetPath);
try { try {
await fs.promises.access(parentDir); await fs.promises.access(parentDir);

View File

@@ -13,7 +13,7 @@ function sanitizeGitError(message, token) {
} }
// Configure allowed workspace root (defaults to user's home directory) // Configure allowed workspace root (defaults to user's home directory)
const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir(); export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir();
// System-critical paths that should never be used as workspace directories // System-critical paths that should never be used as workspace directories
export const FORBIDDEN_PATHS = [ export const FORBIDDEN_PATHS = [
@@ -48,7 +48,7 @@ export const FORBIDDEN_PATHS = [
* @param {string} requestedPath - The path to validate * @param {string} requestedPath - The path to validate
* @returns {Promise<{valid: boolean, resolvedPath?: string, error?: string}>} * @returns {Promise<{valid: boolean, resolvedPath?: string, error?: string}>}
*/ */
async function validateWorkspacePath(requestedPath) { export async function validateWorkspacePath(requestedPath) {
try { try {
// Resolve to absolute path // Resolve to absolute path
let absolutePath = path.resolve(requestedPath); let absolutePath = path.resolve(requestedPath);

View File

@@ -31,13 +31,13 @@ function LoginModal({
switch (provider) { switch (provider) {
case 'claude': case 'claude':
return isAuthenticated ? 'claude setup-token --dangerously-skip-permissions' : 'claude /login --dangerously-skip-permissions'; return isAuthenticated ? 'claude setup-token --dangerously-skip-permissions' : 'claude /exit --dangerously-skip-permissions --no-session-persistence';
case 'cursor': case 'cursor':
return 'cursor-agent login'; return 'cursor-agent login';
case 'codex': case 'codex':
return isPlatform ? 'codex login --device-auth' : 'codex login'; return isPlatform ? 'codex login --device-auth' : 'codex login';
default: default:
return isAuthenticated ? 'claude setup-token --dangerously-skip-permissions' : 'claude /login --dangerously-skip-permissions'; return isAuthenticated ? 'claude setup-token --dangerously-skip-permissions' : 'claude /exit --dangerously-skip-permissions --no-session-persistence';
} }
}; };

View File

@@ -15,7 +15,8 @@ const Onboarding = ({ onComplete }) => {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [activeLoginProvider, setActiveLoginProvider] = useState(null); const [activeLoginProvider, setActiveLoginProvider] = useState(null);
const [selectedProject] = useState({ name: 'default', fullPath: '' }); const isPlatform = import.meta.env.VITE_IS_PLATFORM === 'true';
const [selectedProject] = useState({ name: 'default', fullPath: isPlatform ? '/workspace' : '' });
const [claudeAuthStatus, setClaudeAuthStatus] = useState({ const [claudeAuthStatus, setClaudeAuthStatus] = useState({
authenticated: false, authenticated: false,

View File

@@ -183,7 +183,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
const data = await response.json(); const data = await response.json();
if (!response.ok) { if (!response.ok) {
throw new Error(data.error || t('projectWizard.errors.failedToCreate')); throw new Error(data.details || data.error || t('projectWizard.errors.failedToCreate'));
} }
if (onProjectCreated) { if (onProjectCreated) {