diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts index 4b313bfe..e16fa94c 100644 --- a/src/main/ipc/teams.ts +++ b/src/main/ipc/teams.ts @@ -2945,8 +2945,12 @@ async function handleToolApprovalRespond( async function handleToolApprovalSettings( _event: IpcMainInvokeEvent, + teamName: unknown, settings: unknown ): Promise> { + if (typeof teamName !== 'string' || teamName.trim().length === 0) { + return { success: false, error: 'teamName must be a non-empty string' }; + } if (typeof settings !== 'object' || settings === null) { return { success: false, error: 'Settings must be an object' }; } @@ -2973,7 +2977,10 @@ async function handleToolApprovalSettings( } try { - getTeamProvisioningService().updateToolApprovalSettings(s as unknown as ToolApprovalSettings); + getTeamProvisioningService().updateToolApprovalSettings( + teamName as string, + s as unknown as ToolApprovalSettings + ); } catch (err) { return { success: false, diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 9d263a04..32fcc481 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -1276,7 +1276,7 @@ export class TeamProvisioningService { private helpOutputCache: string | null = null; private helpOutputCacheTime = 0; private static readonly HELP_CACHE_TTL_MS = 5 * 60 * 1000; - private toolApprovalSettings: ToolApprovalSettings = DEFAULT_TOOL_APPROVAL_SETTINGS; + private toolApprovalSettingsByTeam = new Map(); private pendingTimeouts = new Map(); private inFlightResponses = new Set(); private controlApiBaseUrlResolver: (() => Promise) | null = null; @@ -2022,8 +2022,12 @@ export class TeamProvisioningService { this.mainWindowRef = win; } - updateToolApprovalSettings(settings: ToolApprovalSettings): void { - this.toolApprovalSettings = settings; + private getToolApprovalSettings(teamName: string): ToolApprovalSettings { + return this.toolApprovalSettingsByTeam.get(teamName) ?? DEFAULT_TOOL_APPROVAL_SETTINGS; + } + + updateToolApprovalSettings(teamName: string, settings: ToolApprovalSettings): void { + this.toolApprovalSettingsByTeam.set(teamName, settings); this.reEvaluatePendingApprovals(); } @@ -5779,7 +5783,11 @@ export class TeamProvisioningService { }; // Check auto-allow rules before prompting user - const autoResult = shouldAutoAllow(this.toolApprovalSettings, toolName, toolInput); + const autoResult = shouldAutoAllow( + this.getToolApprovalSettings(run.teamName), + toolName, + toolInput + ); if (autoResult.autoAllow) { logger.info(`[${run.teamName}] Auto-allowing ${toolName} (${autoResult.reason})`); this.autoAllowControlRequest(run, requestId); @@ -5832,7 +5840,11 @@ export class TeamProvisioningService { teamDisplayName: run.request.displayName, }; - const autoResult = shouldAutoAllow(this.toolApprovalSettings, perm.toolName, perm.input); + const autoResult = shouldAutoAllow( + this.getToolApprovalSettings(run.teamName), + perm.toolName, + perm.input + ); if (autoResult.autoAllow) { logger.info( `[${run.teamName}] Auto-allowing teammate ${perm.agentId} ${perm.toolName} (${autoResult.reason})` @@ -6003,7 +6015,7 @@ export class TeamProvisioningService { } private startApprovalTimeout(run: ProvisioningRun, requestId: string): void { - const { timeoutAction, timeoutSeconds } = this.toolApprovalSettings; + const { timeoutAction, timeoutSeconds } = this.getToolApprovalSettings(run.teamName); if (timeoutAction === 'wait') return; const timeoutMs = timeoutSeconds * 1000; @@ -6013,7 +6025,7 @@ export class TeamProvisioningService { if (!this.tryClaimResponse(requestId)) return; // Read CURRENT settings (not captured closure) in case user changed action - const currentAction = this.toolApprovalSettings.timeoutAction; + const currentAction = this.getToolApprovalSettings(run.teamName).timeoutAction; if (currentAction === 'wait') { // Settings changed to 'wait' but timer fired before reEvaluatePendingApprovals cleared it this.inFlightResponses.delete(requestId); @@ -6102,13 +6114,10 @@ export class TeamProvisioningService { private reEvaluatePendingApprovals(): void { for (const [, run] of this.runs) { + const settings = this.getToolApprovalSettings(run.teamName); const toRemove: string[] = []; for (const [requestId, approval] of run.pendingApprovals) { - const result = shouldAutoAllow( - this.toolApprovalSettings, - approval.toolName, - approval.toolInput - ); + const result = shouldAutoAllow(settings, approval.toolName, approval.toolInput); if (result.autoAllow) { this.clearApprovalTimeout(requestId); if (!this.tryClaimResponse(requestId)) continue; @@ -6126,16 +6135,10 @@ export class TeamProvisioningService { teamName: run.teamName, reason: 'auto_allow_category', } as ToolApprovalAutoResolved); - } else if ( - this.toolApprovalSettings.timeoutAction !== 'wait' && - !this.pendingTimeouts.has(requestId) - ) { + } else if (settings.timeoutAction !== 'wait' && !this.pendingTimeouts.has(requestId)) { // Settings changed from 'wait' to allow/deny — start timer for already pending items this.startApprovalTimeout(run, requestId); - } else if ( - this.toolApprovalSettings.timeoutAction === 'wait' && - this.pendingTimeouts.has(requestId) - ) { + } else if (settings.timeoutAction === 'wait' && this.pendingTimeouts.has(requestId)) { // Settings changed TO 'wait' — clear existing timers this.clearApprovalTimeout(requestId); } diff --git a/src/preload/index.ts b/src/preload/index.ts index 6caa2728..a8542a5d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1145,8 +1145,8 @@ const electronAPI: ElectronAPI = { ); }; }, - updateToolApprovalSettings: async (settings: ToolApprovalSettings) => { - return invokeIpcWithResult(TEAM_TOOL_APPROVAL_SETTINGS, settings); + updateToolApprovalSettings: async (teamName: string, settings: ToolApprovalSettings) => { + return invokeIpcWithResult(TEAM_TOOL_APPROVAL_SETTINGS, teamName, settings); }, readFileForToolApproval: async (filePath: string) => { return invokeIpcWithResult(TEAM_TOOL_APPROVAL_READ_FILE, filePath); diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts index 6a32b2d0..52602af4 100644 --- a/src/renderer/store/index.ts +++ b/src/renderer/store/index.ts @@ -1064,7 +1064,8 @@ export function initializeNotificationListeners(): () => void { // Sync saved tool approval settings to main process on startup const savedSettings = useStore.getState().toolApprovalSettings; - api.teams.updateToolApprovalSettings?.(savedSettings).catch(() => { + const activeTeam = useStore.getState().selectedTeamName ?? '__global__'; + api.teams.updateToolApprovalSettings?.(activeTeam, savedSettings).catch(() => { // Silently ignore — settings will use defaults until next update }); } diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index cfc58378..631a5a74 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -767,10 +767,11 @@ function extractBaseModel(raw?: string): string | undefined { return raw.replace(/\[1m\]$/, '') || undefined; } -function loadToolApprovalSettings(): ToolApprovalSettings { +const TOOL_APPROVAL_PREFIX = 'team:toolApprovalSettings:'; + +function parseToolApprovalSettings(raw: string | null): ToolApprovalSettings { + if (!raw) return DEFAULT_TOOL_APPROVAL_SETTINGS; try { - const raw = localStorage.getItem('team:toolApprovalSettings'); - if (!raw) return DEFAULT_TOOL_APPROVAL_SETTINGS; const parsed = JSON.parse(raw) as Record; const d = DEFAULT_TOOL_APPROVAL_SETTINGS; return { @@ -801,6 +802,23 @@ function loadToolApprovalSettings(): ToolApprovalSettings { } } +function loadToolApprovalSettingsForTeam(teamName: string): ToolApprovalSettings { + return parseToolApprovalSettings(localStorage.getItem(TOOL_APPROVAL_PREFIX + teamName)); +} + +function saveToolApprovalSettingsForTeam(teamName: string, settings: ToolApprovalSettings): void { + try { + localStorage.setItem(TOOL_APPROVAL_PREFIX + teamName, JSON.stringify(settings)); + } catch { + // best-effort + } +} + +/** Load global settings (legacy fallback for first load / no team selected). */ +function loadToolApprovalSettings(): ToolApprovalSettings { + return parseToolApprovalSettings(localStorage.getItem('team:toolApprovalSettings')); +} + export const createTeamSlice: StateCreator = (set, get) => ({ teams: [], teamByName: {}, @@ -1276,6 +1294,8 @@ export const createTeamSlice: StateCreator = (set, selectedTeamLoadNonce: requestNonce, selectedTeamError: null, reviewActionError: null, + // Load per-team tool approval settings + toolApprovalSettings: loadToolApprovalSettingsForTeam(teamName), }); try { @@ -1848,11 +1868,13 @@ export const createTeamSlice: StateCreator = (set, }, }, })); - // When launching WITHOUT auto-approve, reset the global autoAllowAll flag - // so the user sees the ToolApprovalSheet for this team's tool requests. - if (request.skipPermissions === false && get().toolApprovalSettings.autoAllowAll) { - await get().updateToolApprovalSettings({ autoAllowAll: false }); - } + // Initialize per-team tool approval settings based on skipPermissions flag + const initialSettings: ToolApprovalSettings = + request.skipPermissions === false + ? DEFAULT_TOOL_APPROVAL_SETTINGS + : { ...DEFAULT_TOOL_APPROVAL_SETTINGS, autoAllowAll: true }; + saveToolApprovalSettingsForTeam(request.teamName, initialSettings); + set({ toolApprovalSettings: initialSettings }); try { if (typeof api.teams.createTeam !== 'function') { throw new Error( @@ -2025,9 +2047,14 @@ export const createTeamSlice: StateCreator = (set, [request.teamName]: pendingRunId, }, })); - // When launching WITHOUT auto-approve, reset the global autoAllowAll flag - if (request.skipPermissions === false && get().toolApprovalSettings.autoAllowAll) { - await get().updateToolApprovalSettings({ autoAllowAll: false }); + // Initialize per-team tool approval settings based on skipPermissions flag + { + const launchSettings: ToolApprovalSettings = + request.skipPermissions === false + ? DEFAULT_TOOL_APPROVAL_SETTINGS + : { ...DEFAULT_TOOL_APPROVAL_SETTINGS, autoAllowAll: true }; + saveToolApprovalSettingsForTeam(request.teamName, launchSettings); + set({ toolApprovalSettings: launchSettings }); } try { const response = await unwrapIpc('team:launch', () => api.teams.launchTeam(request)); @@ -2321,12 +2348,18 @@ export const createTeamSlice: StateCreator = (set, }, updateToolApprovalSettings: async (patch) => { + const teamName = get().selectedTeamName; const current = get().toolApprovalSettings; const merged = { ...current, ...patch }; set({ toolApprovalSettings: merged }); - localStorage.setItem('team:toolApprovalSettings', JSON.stringify(merged)); + // Save per-team if a team is selected, otherwise global fallback + if (teamName) { + saveToolApprovalSettingsForTeam(teamName, merged); + } else { + localStorage.setItem('team:toolApprovalSettings', JSON.stringify(merged)); + } try { - await api.teams.updateToolApprovalSettings(merged); + await api.teams.updateToolApprovalSettings(teamName ?? '__global__', merged); } catch (err) { logger.warn('Failed to sync tool approval settings to main:', err); } diff --git a/src/shared/types/api.ts b/src/shared/types/api.ts index b0485fa0..e52ab6ea 100644 --- a/src/shared/types/api.ts +++ b/src/shared/types/api.ts @@ -548,7 +548,7 @@ export interface TeamsAPI { ) => Promise; validateCliArgs: (rawArgs: string) => Promise; onToolApprovalEvent: (callback: (event: unknown, data: ToolApprovalEvent) => void) => () => void; - updateToolApprovalSettings: (settings: ToolApprovalSettings) => Promise; + updateToolApprovalSettings: (teamName: string, settings: ToolApprovalSettings) => Promise; readFileForToolApproval: (filePath: string) => Promise; }