diff --git a/apps/ui/src/components/views/settings-view/hooks/use-cli-status.ts b/apps/ui/src/components/views/settings-view/hooks/use-cli-status.ts index ac97040a..5afaf5f7 100644 --- a/apps/ui/src/components/views/settings-view/hooks/use-cli-status.ts +++ b/apps/ui/src/components/views/settings-view/hooks/use-cli-status.ts @@ -32,6 +32,53 @@ export function useCliStatus() { const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false); + // Refresh Claude auth status from the server + const refreshAuthStatus = useCallback(async () => { + const api = getElectronAPI(); + if (!api?.setup?.getClaudeStatus) return; + + try { + const result = await api.setup.getClaudeStatus(); + if (result.success && result.auth) { + // Cast to extended type that includes server-added fields + const auth = result.auth as typeof result.auth & { + oauthTokenValid?: boolean; + apiKeyValid?: boolean; + }; + // Map server method names to client method types + // Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none + const validMethods = [ + 'oauth_token_env', + 'oauth_token', + 'api_key', + 'api_key_env', + 'credentials_file', + 'cli_authenticated', + 'none', + ] as const; + type AuthMethod = (typeof validMethods)[number]; + const method: AuthMethod = validMethods.includes(auth.method as AuthMethod) + ? (auth.method as AuthMethod) + : auth.authenticated + ? 'api_key' + : 'none'; // Default authenticated to api_key, not none + const authStatus = { + authenticated: auth.authenticated, + method, + hasCredentialsFile: auth.hasCredentialsFile ?? false, + oauthTokenValid: + auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken, + apiKeyValid: auth.apiKeyValid || auth.hasStoredApiKey || auth.hasEnvApiKey, + hasEnvOAuthToken: auth.hasEnvOAuthToken, + hasEnvApiKey: auth.hasEnvApiKey, + }; + setClaudeAuthStatus(authStatus); + } + } catch (error) { + logger.error('Failed to refresh Claude auth status:', error); + } + }, [setClaudeAuthStatus]); + // Check CLI status on mount useEffect(() => { const checkCliStatus = async () => { @@ -48,52 +95,11 @@ export function useCliStatus() { } // Check Claude auth status (re-fetch on mount to ensure persistence) - if (api?.setup?.getClaudeStatus) { - try { - const result = await api.setup.getClaudeStatus(); - if (result.success && result.auth) { - // Cast to extended type that includes server-added fields - const auth = result.auth as typeof result.auth & { - oauthTokenValid?: boolean; - apiKeyValid?: boolean; - }; - // Map server method names to client method types - // Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none - const validMethods = [ - 'oauth_token_env', - 'oauth_token', - 'api_key', - 'api_key_env', - 'credentials_file', - 'cli_authenticated', - 'none', - ] as const; - type AuthMethod = (typeof validMethods)[number]; - const method: AuthMethod = validMethods.includes(auth.method as AuthMethod) - ? (auth.method as AuthMethod) - : auth.authenticated - ? 'api_key' - : 'none'; // Default authenticated to api_key, not none - const authStatus = { - authenticated: auth.authenticated, - method, - hasCredentialsFile: auth.hasCredentialsFile ?? false, - oauthTokenValid: - auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken, - apiKeyValid: auth.apiKeyValid || auth.hasStoredApiKey || auth.hasEnvApiKey, - hasEnvOAuthToken: auth.hasEnvOAuthToken, - hasEnvApiKey: auth.hasEnvApiKey, - }; - setClaudeAuthStatus(authStatus); - } - } catch (error) { - logger.error('Failed to check Claude auth status:', error); - } - } + await refreshAuthStatus(); }; checkCliStatus(); - }, [setClaudeAuthStatus]); + }, [refreshAuthStatus]); // Refresh Claude CLI status and auth status const handleRefreshClaudeCli = useCallback(async () => { @@ -105,51 +111,13 @@ export function useCliStatus() { setClaudeCliStatus(status); } // Also refresh auth status - if (api?.setup?.getClaudeStatus) { - try { - const result = await api.setup.getClaudeStatus(); - if (result.success && result.auth) { - const auth = result.auth as typeof result.auth & { - oauthTokenValid?: boolean; - apiKeyValid?: boolean; - }; - const validMethods = [ - 'oauth_token_env', - 'oauth_token', - 'api_key', - 'api_key_env', - 'credentials_file', - 'cli_authenticated', - 'none', - ] as const; - type AuthMethod = (typeof validMethods)[number]; - const method: AuthMethod = validMethods.includes(auth.method as AuthMethod) - ? (auth.method as AuthMethod) - : auth.authenticated - ? 'api_key' - : 'none'; - const authStatus = { - authenticated: auth.authenticated, - method, - hasCredentialsFile: auth.hasCredentialsFile ?? false, - oauthTokenValid: - auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken, - apiKeyValid: auth.apiKeyValid || auth.hasStoredApiKey || auth.hasEnvApiKey, - hasEnvOAuthToken: auth.hasEnvOAuthToken, - hasEnvApiKey: auth.hasEnvApiKey, - }; - setClaudeAuthStatus(authStatus); - } - } catch (error) { - logger.error('Failed to refresh Claude auth status:', error); - } - } + await refreshAuthStatus(); } catch (error) { logger.error('Failed to refresh Claude CLI status:', error); } finally { setIsCheckingClaudeCli(false); } - }, [setClaudeAuthStatus]); + }, [refreshAuthStatus]); return { claudeCliStatus, diff --git a/dev.mjs b/dev.mjs index ea549c65..f22a68e4 100644 --- a/dev.mjs +++ b/dev.mjs @@ -47,6 +47,14 @@ const processes = { docker: null, }; +/** + * Sanitize a project name to be safe for use in shell commands and Docker image names. + * Converts to lowercase and removes any characters that aren't alphanumeric. + */ +function sanitizeProjectName(name) { + return name.toLowerCase().replace(/[^a-z0-9]/g, ''); +} + /** * Check if Docker images need to be rebuilt based on Dockerfile or package.json changes */ @@ -60,35 +68,27 @@ function shouldRebuildDockerImages() { const packageJsonMtime = statSync(packageJsonPath).mtimeMs; const latestSourceMtime = Math.max(dockerfileMtime, packageJsonMtime); - // Get image names from docker-compose config - let serverImageName, uiImageName; + // Get project name from docker-compose config, falling back to directory name + let projectName; try { const composeConfig = execSync('docker compose config --format json', { encoding: 'utf-8', cwd: __dirname, }); const config = JSON.parse(composeConfig); - - // Docker Compose generates image names as _ - // Get project name from config or default to directory name - const projectName = - config.name || - path - .basename(__dirname) - .toLowerCase() - .replace(/[^a-z0-9]/g, ''); - serverImageName = `${projectName}_server`; - uiImageName = `${projectName}_ui`; + projectName = config.name; } catch (error) { - // Fallback to default naming convention - const projectName = path - .basename(__dirname) - .toLowerCase() - .replace(/[^a-z0-9]/g, ''); - serverImageName = `${projectName}_server`; - uiImageName = `${projectName}_ui`; + // Fallback handled below } + // Sanitize project name (whether from config or fallback) + // This prevents command injection and ensures valid Docker image names + const sanitizedProjectName = sanitizeProjectName( + projectName || path.basename(__dirname) + ); + const serverImageName = `${sanitizedProjectName}_server`; + const uiImageName = `${sanitizedProjectName}_ui`; + // Check if images exist and get their creation times let needsRebuild = false; diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index a13c4553..017213dc 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -17,19 +17,13 @@ fi chown -R automaker:automaker /home/automaker/.claude chmod 700 /home/automaker/.claude -# Fix permissions on Cursor CLI config directory if it exists -# This handles the case where a volume is mounted and owned by root -if [ -d "/home/automaker/.cursor" ]; then - chown -R automaker:automaker /home/automaker/.cursor - chmod -R 700 /home/automaker/.cursor -fi - -# Ensure the directory exists with correct permissions if volume is empty +# Ensure Cursor CLI config directory exists with correct permissions +# This handles both: mounted volumes (owned by root) and empty directories if [ ! -d "/home/automaker/.cursor" ]; then mkdir -p /home/automaker/.cursor - chown automaker:automaker /home/automaker/.cursor - chmod 700 /home/automaker/.cursor fi +chown -R automaker:automaker /home/automaker/.cursor +chmod -R 700 /home/automaker/.cursor # If CURSOR_AUTH_TOKEN is set, write it to the cursor auth file # On Linux, cursor-agent uses ~/.config/cursor/auth.json for file-based credential storage diff --git a/docs/docker-isolation.md b/docs/docker-isolation.md index af190d9d..eb8fe7e1 100644 --- a/docs/docker-isolation.md +++ b/docs/docker-isolation.md @@ -78,7 +78,7 @@ echo "CURSOR_AUTH_TOKEN=$(./scripts/get-cursor-token.sh)" >> .env **Note**: The cursor-agent CLI stores its OAuth tokens separately from the Cursor IDE: - **macOS**: Tokens are stored in Keychain (service: `cursor-access-token`) -- **Linux**: Tokens are stored in `~/.config/cursor/auth.json` +- **Linux**: Tokens are stored in `~/.config/cursor/auth.json` (not `~/.cursor`) ### Apply to container diff --git a/start.mjs b/start.mjs index 80ed4746..29a6e04e 100755 --- a/start.mjs +++ b/start.mjs @@ -56,6 +56,14 @@ const processes = { docker: null, }; +/** + * Sanitize a project name to be safe for use in shell commands and Docker image names. + * Converts to lowercase and removes any characters that aren't alphanumeric. + */ +function sanitizeProjectName(name) { + return name.toLowerCase().replace(/[^a-z0-9]/g, ''); +} + /** * Check if Docker images need to be rebuilt based on Dockerfile or package.json changes */ @@ -69,35 +77,27 @@ function shouldRebuildDockerImages() { const packageJsonMtime = statSync(packageJsonPath).mtimeMs; const latestSourceMtime = Math.max(dockerfileMtime, packageJsonMtime); - // Get image names from docker-compose config - let serverImageName, uiImageName; + // Get project name from docker-compose config, falling back to directory name + let projectName; try { const composeConfig = execSync('docker compose config --format json', { encoding: 'utf-8', cwd: __dirname, }); const config = JSON.parse(composeConfig); - - // Docker Compose generates image names as _ - // Get project name from config or default to directory name - const projectName = - config.name || - path - .basename(__dirname) - .toLowerCase() - .replace(/[^a-z0-9]/g, ''); - serverImageName = `${projectName}_server`; - uiImageName = `${projectName}_ui`; + projectName = config.name; } catch (error) { - // Fallback to default naming convention - const projectName = path - .basename(__dirname) - .toLowerCase() - .replace(/[^a-z0-9]/g, ''); - serverImageName = `${projectName}_server`; - uiImageName = `${projectName}_ui`; + // Fallback handled below } + // Sanitize project name (whether from config or fallback) + // This prevents command injection and ensures valid Docker image names + const sanitizedProjectName = sanitizeProjectName( + projectName || path.basename(__dirname) + ); + const serverImageName = `${sanitizedProjectName}_server`; + const uiImageName = `${sanitizedProjectName}_ui`; + // Check if images exist and get their creation times let needsRebuild = false;