feat(team): per-team tool approval settings
Tool approval settings (autoAllowAll, autoAllowFileEdits, etc.) are
now stored per-team instead of globally. Clicking 'Allow all' in one
team no longer affects other teams.
- localStorage key: 'team:toolApprovalSettings:{teamName}'
- Settings loaded on team select, initialized on create/launch
- skipPermissions=false -> defaults (autoAllowAll:false)
- skipPermissions=true -> autoAllowAll:true for that team
- Main process: Map<teamName, settings> instead of single instance
- IPC: teamName parameter added to updateToolApprovalSettings
This commit is contained in:
parent
8c08087337
commit
58bd7cc507
6 changed files with 82 additions and 38 deletions
|
|
@ -2945,8 +2945,12 @@ async function handleToolApprovalRespond(
|
|||
|
||||
async function handleToolApprovalSettings(
|
||||
_event: IpcMainInvokeEvent,
|
||||
teamName: unknown,
|
||||
settings: unknown
|
||||
): Promise<IpcResult<void>> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<string, ToolApprovalSettings>();
|
||||
private pendingTimeouts = new Map<string, NodeJS.Timeout>();
|
||||
private inFlightResponses = new Set<string>();
|
||||
private controlApiBaseUrlResolver: (() => Promise<string | null>) | 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1145,8 +1145,8 @@ const electronAPI: ElectronAPI = {
|
|||
);
|
||||
};
|
||||
},
|
||||
updateToolApprovalSettings: async (settings: ToolApprovalSettings) => {
|
||||
return invokeIpcWithResult<void>(TEAM_TOOL_APPROVAL_SETTINGS, settings);
|
||||
updateToolApprovalSettings: async (teamName: string, settings: ToolApprovalSettings) => {
|
||||
return invokeIpcWithResult<void>(TEAM_TOOL_APPROVAL_SETTINGS, teamName, settings);
|
||||
},
|
||||
readFileForToolApproval: async (filePath: string) => {
|
||||
return invokeIpcWithResult<ToolApprovalFileContent>(TEAM_TOOL_APPROVAL_READ_FILE, filePath);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
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<AppState, [], [], TeamSlice> = (set, get) => ({
|
||||
teams: [],
|
||||
teamByName: {},
|
||||
|
|
@ -1276,6 +1294,8 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (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<AppState, [], [], TeamSlice> = (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<AppState, [], [], TeamSlice> = (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<AppState, [], [], TeamSlice> = (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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@ export interface TeamsAPI {
|
|||
) => Promise<void>;
|
||||
validateCliArgs: (rawArgs: string) => Promise<CliArgsValidationResult>;
|
||||
onToolApprovalEvent: (callback: (event: unknown, data: ToolApprovalEvent) => void) => () => void;
|
||||
updateToolApprovalSettings: (settings: ToolApprovalSettings) => Promise<void>;
|
||||
updateToolApprovalSettings: (teamName: string, settings: ToolApprovalSettings) => Promise<void>;
|
||||
readFileForToolApproval: (filePath: string) => Promise<ToolApprovalFileContent>;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue