feat(automation): add macOS permission check before automation start
This commit is contained in:
@@ -3,6 +3,7 @@ import { MockBrowserAutomationAdapter } from '@/packages/infrastructure/adapters
|
||||
import { BrowserDevToolsAdapter } from '@/packages/infrastructure/adapters/automation/BrowserDevToolsAdapter';
|
||||
import { NutJsAutomationAdapter } from '@/packages/infrastructure/adapters/automation/NutJsAutomationAdapter';
|
||||
import { MockAutomationEngineAdapter } from '@/packages/infrastructure/adapters/automation/MockAutomationEngineAdapter';
|
||||
import { PermissionService } from '@/packages/infrastructure/adapters/automation/PermissionService';
|
||||
import { StartAutomationSessionUseCase } from '@/packages/application/use-cases/StartAutomationSessionUseCase';
|
||||
import { loadAutomationConfig, AutomationMode } from '@/packages/infrastructure/config';
|
||||
import { PinoLogAdapter } from '@/packages/infrastructure/adapters/logging/PinoLogAdapter';
|
||||
@@ -74,6 +75,7 @@ export class DIContainer {
|
||||
private automationEngine: IAutomationEngine;
|
||||
private startAutomationUseCase: StartAutomationSessionUseCase;
|
||||
private automationMode: AutomationMode;
|
||||
private permissionService: PermissionService;
|
||||
|
||||
private constructor() {
|
||||
// Initialize logger first - it's needed by other components
|
||||
@@ -94,6 +96,9 @@ export class DIContainer {
|
||||
this.browserAutomation,
|
||||
this.sessionRepository
|
||||
);
|
||||
this.permissionService = new PermissionService(
|
||||
this.logger.child({ service: 'PermissionService' })
|
||||
);
|
||||
|
||||
this.logger.info('DIContainer initialized', {
|
||||
automationMode: config.mode,
|
||||
@@ -141,6 +146,10 @@ export class DIContainer {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
public getPermissionService(): PermissionService {
|
||||
return this.permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize browser connection for dev mode.
|
||||
* In dev mode, connects to the browser via Chrome DevTools Protocol.
|
||||
|
||||
@@ -11,8 +11,62 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
|
||||
const startAutomationUseCase = container.getStartAutomationUseCase();
|
||||
const sessionRepository = container.getSessionRepository();
|
||||
const automationEngine = container.getAutomationEngine();
|
||||
const permissionService = container.getPermissionService();
|
||||
const logger = container.getLogger();
|
||||
|
||||
// Permission handlers
|
||||
ipcMain.handle('automation:checkPermissions', async () => {
|
||||
try {
|
||||
const result = await permissionService.checkPermissions();
|
||||
return {
|
||||
success: true,
|
||||
granted: result.granted,
|
||||
status: result.status,
|
||||
missingPermissions: result.missingPermissions,
|
||||
};
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error('Unknown error');
|
||||
logger.error('Permission check failed', err);
|
||||
return {
|
||||
success: false,
|
||||
error: err.message,
|
||||
granted: false,
|
||||
status: {
|
||||
accessibility: false,
|
||||
screenRecording: false,
|
||||
platform: process.platform,
|
||||
},
|
||||
missingPermissions: ['Accessibility', 'Screen Recording'],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('automation:requestAccessibility', async () => {
|
||||
try {
|
||||
const granted = permissionService.requestAccessibilityPermission();
|
||||
return { success: true, granted };
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error('Unknown error');
|
||||
logger.error('Accessibility permission request failed', err);
|
||||
return { success: false, granted: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('automation:openPermissionSettings', async (_event: IpcMainInvokeEvent, pane?: 'accessibility' | 'screenRecording') => {
|
||||
try {
|
||||
if (pane) {
|
||||
await permissionService.openSystemPreferences(pane);
|
||||
} else {
|
||||
await permissionService.openPermissionsSettings();
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error('Unknown error');
|
||||
logger.error('Failed to open permission settings', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('start-automation', async (_event: IpcMainInvokeEvent, config: HostedSessionConfig) => {
|
||||
try {
|
||||
logger.info('Starting automation', { sessionName: config.sessionName });
|
||||
@@ -22,6 +76,20 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
|
||||
clearInterval(progressMonitorInterval);
|
||||
progressMonitorInterval = null;
|
||||
}
|
||||
|
||||
// Check permissions before starting automation (macOS only)
|
||||
const permissionResult = await permissionService.checkPermissions();
|
||||
if (!permissionResult.granted) {
|
||||
logger.warn('Automation blocked due to missing permissions', {
|
||||
missingPermissions: permissionResult.missingPermissions,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: `Missing required permissions: ${permissionResult.missingPermissions.join(', ')}. Please grant permissions in System Preferences and try again.`,
|
||||
permissionError: true,
|
||||
missingPermissions: permissionResult.missingPermissions,
|
||||
};
|
||||
}
|
||||
|
||||
// Connect to browser first (required for dev mode)
|
||||
const connectionResult = await container.initializeBrowserConnection();
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import type { HostedSessionConfig } from '../../../packages/domain/entities/HostedSessionConfig';
|
||||
|
||||
export interface PermissionStatus {
|
||||
accessibility: boolean;
|
||||
screenRecording: boolean;
|
||||
platform: NodeJS.Platform;
|
||||
}
|
||||
|
||||
export interface PermissionCheckResponse {
|
||||
success: boolean;
|
||||
granted: boolean;
|
||||
status: PermissionStatus;
|
||||
missingPermissions: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
startAutomation: (config: HostedSessionConfig) => Promise<{ success: boolean; sessionId?: string; error?: string }>;
|
||||
startAutomation: (config: HostedSessionConfig) => Promise<{
|
||||
success: boolean;
|
||||
sessionId?: string;
|
||||
error?: string;
|
||||
permissionError?: boolean;
|
||||
missingPermissions?: string[];
|
||||
}>;
|
||||
stopAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
getSessionStatus: (sessionId: string) => Promise<any>;
|
||||
pauseAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
resumeAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
onSessionProgress: (callback: (progress: any) => void) => void;
|
||||
// Permission APIs
|
||||
checkPermissions: () => Promise<PermissionCheckResponse>;
|
||||
requestAccessibility: () => Promise<{ success: boolean; granted: boolean; error?: string }>;
|
||||
openPermissionSettings: (pane?: 'accessibility' | 'screenRecording') => Promise<{ success: boolean; error?: string }>;
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
@@ -18,5 +42,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
resumeAutomation: (sessionId: string) => ipcRenderer.invoke('resume-automation', sessionId),
|
||||
onSessionProgress: (callback: (progress: any) => void) => {
|
||||
ipcRenderer.on('session-progress', (_event, progress) => callback(progress));
|
||||
}
|
||||
},
|
||||
// Permission APIs
|
||||
checkPermissions: () => ipcRenderer.invoke('automation:checkPermissions'),
|
||||
requestAccessibility: () => ipcRenderer.invoke('automation:requestAccessibility'),
|
||||
openPermissionSettings: (pane?: 'accessibility' | 'screenRecording') =>
|
||||
ipcRenderer.invoke('automation:openPermissionSettings', pane),
|
||||
} as ElectronAPI);
|
||||
Reference in New Issue
Block a user